diff --git a/src/bin.rs b/src/bin.rs index 7d71d7912..9e97cc716 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -197,6 +197,8 @@ fn main() -> std::result::Result<(), std::io::Error> { let signals = &[ /* Catch SIGWINCH to handle terminal resizing */ signal_hook::SIGWINCH, + /* Catch SIGCHLD to handle embed applications status change */ + signal_hook::SIGCHLD, ]; let signal_recvr = notify(signals, sender)?; @@ -303,15 +305,16 @@ fn main() -> std::result::Result<(), std::io::Error> { }, } }, - UIMode::Embed => { - state.rcv_event(UIEvent::EmbedInput(k)); - state.redraw(); - }, + UIMode::Embed => {}, UIMode::Fork => { break 'inner; // `goto` 'reap loop, and wait on child. }, } }, + ThreadEvent::InputRaw(raw_input) => { + state.rcv_event(UIEvent::EmbedInput(raw_input)); + state.redraw(); + }, ThreadEvent::RefreshMailbox(event) => { state.refresh_event(*event); state.redraw(); diff --git a/ui/src/components/mail/compose.rs b/ui/src/components/mail/compose.rs index 547a274e5..0319a3f34 100644 --- a/ui/src/components/mail/compose.rs +++ b/ui/src/components/mail/compose.rs @@ -21,10 +21,12 @@ use super::*; -use crate::terminal::embed::EmbedPty; +use crate::terminal::embed::EmbedGrid; use melib::Draft; use mime_apps::query_mime_info; +use nix::sys::wait::WaitStatus; use std::str::FromStr; +use std::sync::{Arc, Mutex}; #[derive(Debug, PartialEq)] enum Cursor { @@ -33,6 +35,31 @@ enum Cursor { //Attachments, } +#[derive(Debug)] +enum EmbedStatus { + Stopped(Arc>, File), + Running(Arc>, File), +} + +impl std::ops::Deref for EmbedStatus { + type Target = Arc>; + fn deref(&self) -> &Arc> { + use EmbedStatus::*; + match self { + Stopped(ref e, _) | Running(ref e, _) => e, + } + } +} + +impl std::ops::DerefMut for EmbedStatus { + fn deref_mut(&mut self) -> &mut Arc> { + use EmbedStatus::*; + match self { + Stopped(ref mut e, _) | Running(ref mut e, _) => e, + } + } +} + #[derive(Debug)] pub struct Composer { reply_context: Option<((usize, usize), Box)>, // (folder_index, thread_node_index) @@ -47,7 +74,7 @@ pub struct Composer { mode: ViewMode, body_area: Area, // Cache body_area in case we need to replace it with a pseudoterminal - embed: Option, + embed: Option, sign_mail: ToggleFlag, dirty: bool, has_changes: bool, @@ -83,6 +110,7 @@ impl Default for Composer { enum ViewMode { Discard(Uuid, Selector), Edit, + Embed, SelectRecipients(Selector
), ThreadView, } @@ -103,6 +131,14 @@ impl ViewMode { false } } + + fn is_embed(&self) -> bool { + if let ViewMode::Embed = self { + true + } else { + false + } + } } impl fmt::Display for Composer { @@ -217,10 +253,9 @@ impl Composer { fn update_draft(&mut self) { let header_values = self.form.values_mut(); let draft_header_map = self.draft.headers_mut(); - /* avoid extra allocations by updating values instead of inserting */ for (k, v) in draft_header_map.iter_mut() { - if let Some(vn) = header_values.remove(k) { - std::mem::swap(v, &mut vn.into_string()); + if let Some(ref vn) = header_values.get(k) { + *v = vn.as_str().to_string(); } } } @@ -510,8 +545,48 @@ impl Component for Composer { /* Regardless of view mode, do the following */ self.form.draw(grid, header_area, context); - self.pager.set_dirty(); - self.pager.draw(grid, body_area, context); + if let Some(ref mut embed_pty) = self.embed { + if self.dirty { + clear_area(grid, body_area); + match embed_pty { + EmbedStatus::Running(_, _) => { + let mut guard = embed_pty.lock().unwrap(); + copy_area( + grid, + &guard.grid, + body_area, + ((0, 0), pos_dec(guard.terminal_size, (1, 1))), + ); + if body_area != self.body_area { + guard.set_terminal_size((width!(body_area), height!(body_area))); + } + context.dirty_areas.push_back(body_area); + self.dirty = false; + return; + } + EmbedStatus::Stopped(_, _) => { + write_string_to_grid( + "process has stopped, press 'e' to re-activate", + grid, + Color::Default, + Color::Default, + Attr::Default, + body_area, + false, + ); + context.dirty_areas.push_back(body_area); + self.dirty = false; + return; + } + } + } + } else { + self.pager.set_dirty(); + self.pager.draw(grid, body_area, context); + } + + self.body_area = body_area; + if self.cursor == Cursor::Body { change_colors( grid, @@ -533,28 +608,9 @@ impl Component for Composer { Color::Default, ); } - self.body_area = body_area; - if let Some(ref mut embed_pty) = self.embed { - let lock = embed_pty.grid.lock().unwrap(); - copy_area( - grid, - &lock, - area, - ((0, 0), pos_dec(embed_pty.terminal_size, (1, 1))), - ); - for y in 0..embed_pty.terminal_size.1 { - for x in 0..embed_pty.terminal_size.0 { - if lock[(x, y)].ch() != ' ' { - debug!("coors {:?} char = {}", (x, y), lock[(x, y)].ch()); - } - } - } - context.dirty_areas.push_back(area); - debug!("copied grid"); - } match self.mode { - ViewMode::ThreadView | ViewMode::Edit => {} + ViewMode::ThreadView | ViewMode::Edit | ViewMode::Embed => {} ViewMode::SelectRecipients(ref mut s) => { s.draw(grid, center_area(area, s.content.size()), context); } @@ -769,31 +825,158 @@ impl Component for Composer { } return true; } - UIEvent::EmbedInput(Key::Char(c)) => { - debug!("got embed input {:?}", event); - let mut buf: [u8; 4] = [0; 4]; - let s = c.encode_utf8(&mut buf); + UIEvent::ChildStatusExited(ref pid, ref exit_code) + if self.embed.is_some() + && *pid + == self + .embed + .as_ref() + .map(|e| e.lock().unwrap().child_pid) + .unwrap() => + { + self.embed = None; + self.mode = ViewMode::Edit; + context + .replies + .push_back(UIEvent::ChangeMode(UIMode::Normal)); + } + UIEvent::ChildStatusStopped(ref pid) + if self.embed.is_some() + && *pid + == self + .embed + .as_ref() + .map(|e| e.lock().unwrap().child_pid) + .unwrap() => + { + match self.embed.take() { + Some(EmbedStatus::Running(e, f)) | Some(EmbedStatus::Stopped(e, f)) => { + self.embed = Some(EmbedStatus::Stopped(e, f)); + } + _ => {} + } + context + .replies + .push_back(UIEvent::ChangeMode(UIMode::Normal)); + } + UIEvent::ChildStatusContinued(ref pid) + if self.embed.is_some() + && *pid + == self + .embed + .as_ref() + .map(|e| e.lock().unwrap().child_pid) + .unwrap() => + { + match self.embed.take() { + Some(EmbedStatus::Running(e, f)) | Some(EmbedStatus::Stopped(e, f)) => { + self.embed = Some(EmbedStatus::Running(e, f)); + } + _ => {} + } + context + .replies + .push_back(UIEvent::ChangeMode(UIMode::Embed)); + } + UIEvent::EmbedInput((Key::Ctrl('z'), _)) => { + self.embed.as_ref().unwrap().lock().unwrap().stop(); + context + .replies + .push_back(UIEvent::ChangeMode(UIMode::Normal)); + } + UIEvent::EmbedInput((ref k, ref b)) => { use std::io::Write; - self.embed - .as_mut() - .unwrap() - .stdin - .write_all(s.as_bytes()) - .unwrap(); + if let Some(ref mut embed) = self.embed { + let mut embed_guard = embed.lock().unwrap(); + if embed_guard.stdin.write_all(b).is_err() { + match embed_guard.is_active() { + Ok(WaitStatus::Exited(_, exit_code)) => { + drop(embed_guard); + if exit_code != 0 { + context.replies.push_back(UIEvent::Notification( + None, + format!( + "Subprocess has exited with exit code {}", + exit_code + ), + Some(NotificationType::ERROR), + )); + } else if let EmbedStatus::Running(_, f) = embed { + let result = f.read_to_string(); + match Draft::from_str(result.as_str()) { + Ok(mut new_draft) => { + std::mem::swap( + self.draft.attachments_mut(), + new_draft.attachments_mut(), + ); + if self.draft != new_draft { + self.has_changes = true; + } + self.draft = new_draft; + } + Err(_) => { + context.replies.push_back(UIEvent::Notification( + None, + "Could not parse draft headers correctly. The invalid text has been set as the body of your draft".to_string(), + Some(NotificationType::ERROR), + )); + } + } + self.initialized = false; + } + self.embed = None; + self.mode = ViewMode::Edit; + context + .replies + .push_back(UIEvent::ChangeMode(UIMode::Normal)); + } + Ok(WaitStatus::Stopped(_, _)) => { + drop(embed_guard); + match self.embed.take() { + Some(EmbedStatus::Running(e, f)) + | Some(EmbedStatus::Stopped(e, f)) => { + self.embed = Some(EmbedStatus::Stopped(e, f)); + } + _ => {} + } + self.dirty = true; + return true; + } + Ok(WaitStatus::Continued(_)) | Ok(WaitStatus::StillAlive) => { + context + .replies + .push_back(UIEvent::EmbedInput((k.clone(), b.to_vec()))); + return true; + } + e => { + context.replies.push_back(UIEvent::Notification( + None, + format!("Subprocess has exited with reason {:?}", e), + Some(NotificationType::ERROR), + )); + drop(embed_guard); + self.embed = None; + self.mode = ViewMode::Edit; + context + .replies + .push_back(UIEvent::ChangeMode(UIMode::Normal)); + } + } + } + } self.dirty = true; return true; } - UIEvent::Input(Key::Char('e')) => { - /* Edit draft in $EDITOR */ - use std::process::{Command, Stdio}; - self.embed = Some(crate::terminal::embed::create_pty(self.body_area).unwrap()); - self.dirty = true; - debug!("returned"); + UIEvent::Input(Key::Char('e')) if self.mode.is_embed() => { + self.embed.as_ref().unwrap().lock().unwrap().wake_up(); context .replies .push_back(UIEvent::ChangeMode(UIMode::Embed)); return true; + } + UIEvent::Input(Key::Char('e')) => { + /* Edit draft in $EDITOR */ let settings = &context.settings; let editor = if let Some(editor_cmd) = settings.composing.editor_cmd.as_ref() { editor_cmd.to_string() @@ -810,19 +993,37 @@ impl Component for Composer { Ok(v) => v, } }; - /* Kill input thread so that spawned command can be sole receiver of stdin */ - { - context.input_kill(); - } /* update Draft's headers based on form values */ self.update_draft(); let f = create_temp_file( self.draft.to_string().unwrap().as_str().as_bytes(), None, None, - true, + false, ); + if settings.composing.embed { + self.embed = Some(EmbedStatus::Running( + crate::terminal::embed::create_pty( + self.body_area, + [editor, f.path().display().to_string()].join(" "), + ) + .unwrap(), + f, + )); + self.dirty = true; + context + .replies + .push_back(UIEvent::ChangeMode(UIMode::Embed)); + self.mode = ViewMode::Embed; + return true; + } + use std::process::{Command, Stdio}; + /* Kill input thread so that spawned command can be sole receiver of stdin */ + { + context.input_kill(); + } + let parts = split_command!(editor); let (cmd, args) = (parts[0], &parts[1..]); if let Err(e) = Command::new(cmd) diff --git a/ui/src/components/utilities/widgets.rs b/ui/src/components/utilities/widgets.rs index 6d8c41db3..7e0e5404f 100644 --- a/ui/src/components/utilities/widgets.rs +++ b/ui/src/components/utilities/widgets.rs @@ -57,6 +57,13 @@ impl Field { self.as_str().is_empty() } + pub fn to_string(&self) -> String { + match self { + Text(ref s, _) => s.as_str().to_string(), + Choice(ref v, ref cursor) => v[*cursor].clone(), + } + } + pub fn into_string(self) -> String { match self { Text(s, _) => s.into_string(), diff --git a/ui/src/conf/composing.rs b/ui/src/conf/composing.rs index deded5a44..d53c6ce75 100644 --- a/ui/src/conf/composing.rs +++ b/ui/src/conf/composing.rs @@ -18,13 +18,28 @@ * You should have received a copy of the GNU General Public License * along with meli. If not, see . */ +use super::default_vals::{none, true_val}; /// Settings for writing and sending new e-mail -#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct ComposingSettings { /// A command to pipe new emails to /// Required pub mailer_cmd: String, /// Command to launch editor. Can have arguments. Draft filename is given as the last argument. If it's missing, the environment variable $EDITOR is looked up. + #[serde(default = "none")] pub editor_cmd: Option, + /// Embed editor (for terminal interfaces) instead of forking and waiting. + #[serde(default = "true_val")] + pub embed: bool, +} + +impl Default for ComposingSettings { + fn default() -> Self { + ComposingSettings { + mailer_cmd: String::new(), + editor_cmd: None, + embed: true, + } + } } diff --git a/ui/src/state.rs b/ui/src/state.rs index a20480eea..f56bf2286 100644 --- a/ui/src/state.rs +++ b/ui/src/state.rs @@ -44,33 +44,39 @@ use termion::{clear, cursor}; pub type StateStdout = termion::screen::AlternateScreen>; struct InputHandler { - rx: Receiver, - tx: Sender, + rx: Receiver, + tx: Sender, } impl InputHandler { fn restore(&self, tx: Sender) { - let stdin = std::io::stdin(); let rx = self.rx.clone(); thread::Builder::new() .name("input-thread".to_string()) .spawn(move || { get_events( - stdin, |k| { tx.send(ThreadEvent::Input(k)).unwrap(); }, - || { - tx.send(ThreadEvent::UIEvent(UIEvent::ChangeMode(UIMode::Fork))) - .unwrap(); + |i| { + tx.send(ThreadEvent::InputRaw(i)).unwrap(); }, &rx, ) }) .unwrap(); } + fn kill(&self) { - self.tx.send(false).unwrap(); + self.tx.send(InputCommand::Kill).unwrap(); + } + + fn switch_to_raw(&self) { + self.tx.send(InputCommand::Raw).unwrap(); + } + + fn switch_from_raw(&self) { + self.tx.send(InputCommand::NoRaw).unwrap(); } } @@ -98,9 +104,19 @@ impl Context { pub fn replies(&mut self) -> Vec { self.replies.drain(0..).collect() } + pub fn input_kill(&self) { self.input.kill(); } + + pub fn input_from_raw(&self) { + self.input.switch_from_raw(); + } + + pub fn input_to_raw(&self) { + self.input.switch_to_raw(); + } + pub fn restore_input(&self) { self.input.restore(self.sender.clone()); } @@ -584,10 +600,16 @@ impl State { return; } UIEvent::ChangeMode(m) => { + if self.mode == UIMode::Embed { + self.context.input_from_raw(); + } self.context .sender .send(ThreadEvent::UIEvent(UIEvent::ChangeMode(m))) .unwrap(); + if m == UIMode::Embed { + self.context.input_to_raw(); + } } _ => {} } diff --git a/ui/src/terminal/embed.rs b/ui/src/terminal/embed.rs index dddbf6cb6..78f38c81c 100644 --- a/ui/src/terminal/embed.rs +++ b/ui/src/terminal/embed.rs @@ -1,17 +1,27 @@ +use crate::split_command; use crate::terminal::position::Area; +use crate::terminal::position::*; +use melib::log; +use melib::ERROR; + use nix::fcntl::{open, OFlag}; use nix::ioctl_write_ptr_bad; use nix::libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}; use nix::pty::{grantpt, posix_openpt, ptsname, unlockpt, Winsize}; use nix::sys::stat::Mode; -use nix::sys::{stat, wait}; -use nix::unistd::{dup2, fork, setsid, ForkResult}; +use nix::sys::{ + stat, + wait::{self, waitpid}, +}; +use nix::unistd::{dup2, fork, setsid, ForkResult, Pid}; +use std::ffi::CString; use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd}; mod grid; use crate::terminal::cells::{Cell, CellBuffer}; -pub use grid::*; +pub use grid::EmbedGrid; +use grid::*; // ioctl command to set window size of pty: use libc::TIOCSWINSZ; @@ -26,14 +36,7 @@ ioctl_write_ptr_bad!(set_window_size, TIOCSWINSZ, Winsize); static SWITCHALTERNATIVE_1049: &'static [u8] = &[b'1', b'0', b'4', b'9']; -#[derive(Debug)] -pub struct EmbedPty { - pub grid: Arc>, - pub stdin: std::fs::File, - pub terminal_size: (usize, usize), -} - -pub fn create_pty(area: Area) -> nix::Result { +pub fn create_pty(area: Area, command: String) -> nix::Result>> { // Open a new PTY master let master_fd = posix_openpt(OFlag::O_RDWR)?; @@ -48,7 +51,7 @@ pub fn create_pty(area: Area) -> nix::Result { //let _slave_fd = open(Path::new(&slave_name), OFlag::O_RDWR, Mode::empty())?; { let winsize = Winsize { - ws_row: 40, + ws_row: 20, ws_col: 80, ws_xpixel: 0, ws_ypixel: 0, @@ -57,39 +60,66 @@ pub fn create_pty(area: Area) -> nix::Result { let master_fd = master_fd.clone().into_raw_fd(); unsafe { set_window_size(master_fd, &winsize).unwrap() }; } - match fork() { + + let child_pid = match fork() { Ok(ForkResult::Child) => { - setsid().unwrap(); // create new session with child as session leader + /* 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, just like a terminal does - dup2(slave_fd, STDIN_FILENO).unwrap(); - dup2(slave_fd, STDOUT_FILENO).unwrap(); - dup2(slave_fd, STDERR_FILENO).unwrap(); - std::process::Command::new("vim").status().unwrap(); + let child_pid = match fork() { + Ok(ForkResult::Child) => { + // assign stdin, stdout, stderr to the tty, just like a terminal does + dup2(slave_fd, STDIN_FILENO).unwrap(); + dup2(slave_fd, STDOUT_FILENO).unwrap(); + dup2(slave_fd, STDERR_FILENO).unwrap(); + let parts = split_command!(command); + let (cmd, _) = (parts[0], &parts[1..]); + if let Err(e) = nix::unistd::execv( + &CString::new(cmd).unwrap(), + &parts + .iter() + .map(|&a| CString::new(a).unwrap()) + .collect::>(), + ) { + log(format!("Could not execute `{}`: {}", command, e,), ERROR); + std::process::exit(-1); + } + /* This path shouldn't be executed. */ + std::process::exit(0); + } + Ok(ForkResult::Parent { child }) => child, + Err(e) => panic!(e), + }; + waitpid(child_pid, None).unwrap(); + std::process::exit(0); } - Ok(ForkResult::Parent { child: _ }) => {} + Ok(ForkResult::Parent { child }) => child, Err(e) => panic!(e), }; let stdin = unsafe { std::fs::File::from_raw_fd(master_fd.clone().into_raw_fd()) }; - let stdin_ = unsafe { std::fs::File::from_raw_fd(master_fd.clone().into_raw_fd()) }; - let grid = Arc::new(Mutex::new(CellBuffer::new(80, 40, Cell::default()))); + let mut embed_grid = EmbedGrid::new(stdin, child_pid); + embed_grid.set_terminal_size((width!(area), height!(area))); + let grid = Arc::new(Mutex::new(embed_grid)); let grid_ = grid.clone(); - let terminal_size = (80, 40); 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, area, grid_, stdin_); + forward_pty_translate_escape_codes(master_file, grid_); }) .unwrap(); - Ok(EmbedPty { - grid, - stdin, - terminal_size, - }) + Ok(grid) +} + +fn forward_pty_translate_escape_codes(pty_fd: std::fs::File, grid: Arc>) { + let mut bytes_iter = pty_fd.bytes(); + debug!("waiting for bytes"); + while let Some(Ok(byte)) = bytes_iter.next() { + debug!("got byte {}", byte as char); + grid.lock().unwrap().process_byte(byte); + } } #[derive(Debug)] @@ -131,6 +161,7 @@ impl std::fmt::Display for EscCode<'_> { }; } match self { + EscCode(G0, b'B') => write!(f, "ESC(B\t\tG0 USASCII charset set"), EscCode(G0, c) => write!(f, "ESC({}\t\tG0 charset set", *c as char), EscCode(Osc1(ref buf), ref c) => { write!(f, "ESC]{}{}\t\tOSC", unsafestr!(buf), *c as char) @@ -148,13 +179,13 @@ impl std::fmt::Display for EscCode<'_> { ), EscCode(Csi, b'K') => write!( f, - "ESC[K\t\tCSI Erase from the cursor to the end of the line [BAD]" + "ESC[K\t\tCSI Erase from the cursor to the end of the line" ), EscCode(Csi, b'J') => write!( f, - "ESC[J\t\tCSI Erase from the cursor to the end of the screen [BAD]" + "ESC[J\t\tCSI Erase from the cursor to the end of the screen" ), - EscCode(Csi, b'H') => write!(f, "ESC[H\t\tCSI Move the cursor to home position. [BAD]"), + EscCode(Csi, b'H') => write!(f, "ESC[H\t\tCSI Move the cursor to home position."), EscCode(Csi, c) => write!(f, "ESC[{}\t\tCSI [UNKNOWN]", *c as char), EscCode(Csi1(ref buf), b'm') => write!( f, @@ -205,9 +236,31 @@ impl std::fmt::Display for EscCode<'_> { "ESC[{buf}G\t\tCursor Character Absolute [column={buf}] (default = [row,1])", buf = unsafestr!(buf) ), + + EscCode(Csi1(ref buf), b'P') => write!( + f, + "ESC[{buf}P\t\tDelete P s Character(s) (default = 1) (DCH). ", + buf = unsafestr!(buf) + ), + EscCode(Csi1(ref buf), b'S') => write!( + f, + "ESC[{buf}S\t\tCSI P s S Scroll up P s lines (default = 1) (SU), VT420, EC", + buf = unsafestr!(buf) + ), + EscCode(Csi1(ref buf), b'J') => write!( + f, + "Erase in display {buf}", + buf = unsafestr!(buf) + ), EscCode(Csi1(ref buf), c) => { write!(f, "ESC[{}{}\t\tCSI [UNKNOWN]", unsafestr!(buf), *c as char) } + EscCode(Csi2(ref buf1, ref buf2), b'r') => write!( + f, + "ESC[{};{}r\t\tCSI Set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM), VT100.", + unsafestr!(buf1), + unsafestr!(buf2), + ), EscCode(Csi2(ref buf1, ref buf2), c) => write!( f, "ESC[{};{}{}\t\tCSI", @@ -240,16 +293,24 @@ impl std::fmt::Display for EscCode<'_> { "ESC[?{}r\t\tCSI Restore DEC Private Mode Values", unsafestr!(buf) ), - EscCode(CsiQ(ref buf), b'h') if buf == &[b'2', b'5'] => write!( + EscCode(CsiQ(ref buf), b'h') if buf == b"25" => write!( f, "ESC[?25h\t\tCSI DEC Private Mode Set (DECSET) show cursor", ), + EscCode(CsiQ(ref buf), b'h') if buf == b"12" => write!( + f, + "ESC[?12h\t\tCSI DEC Private Mode Set (DECSET) Start Blinking Cursor.", + ), EscCode(CsiQ(ref buf), b'h') => write!( f, "ESC[?{}h\t\tCSI DEC Private Mode Set (DECSET). [UNKNOWN]", unsafestr!(buf) ), - EscCode(CsiQ(ref buf), b'l') if buf == &[b'2', b'5'] => write!( + EscCode(CsiQ(ref buf), b'l') if buf == b"12" => write!( + f, + "ESC[?12l\t\tCSI DEC Private Mode Set (DECSET) Stop Blinking Cursor", + ), + EscCode(CsiQ(ref buf), b'l') if buf == b"25" => write!( f, "ESC[?25l\t\tCSI DEC Private Mode Set (DECSET) hide cursor", ), @@ -260,38 +321,3 @@ impl std::fmt::Display for EscCode<'_> { } } } - -fn forward_pty_translate_escape_codes( - pty_fd: std::fs::File, - area: Area, - grid: Arc>, - stdin: std::fs::File, -) { - let (upper_left, bottom_right) = area; - let (upper_x, upper_y) = upper_left; - let (bottom_x, bottom_y) = bottom_right; - let upper_x_str = upper_x.to_string(); - let upper_y_str = upper_y.to_string(); - let bottom_x_str = bottom_x.to_string(); - let bottom_y_str = bottom_y.to_string(); - - debug!(area); - debug!(&upper_x_str); - debug!(&upper_y_str); - debug!(&bottom_x_str); - debug!(&bottom_y_str); - let mut embed_grid = EmbedGrid::new(grid, stdin); - embed_grid.set_terminal_size((79, 39)); - let mut bytes_iter = pty_fd.bytes(); - let mut prev_char = b'\0'; - debug!("waiting for bytes"); - while let Some(Ok(byte)) = bytes_iter.next() { - debug!("got byte {}", byte as char); - debug!( - "{}{} byte is {} and state is {:?}", - prev_char as char, byte as char, byte as char, &embed_grid.state - ); - prev_char = byte; - embed_grid.process_byte(byte); - } -} diff --git a/ui/src/terminal/embed/grid.rs b/ui/src/terminal/embed/grid.rs index f425273ba..8e3ae8a2d 100644 --- a/ui/src/terminal/embed/grid.rs +++ b/ui/src/terminal/embed/grid.rs @@ -1,28 +1,54 @@ use super::*; -use crate::terminal::cells::{Cell, CellBuffer}; +use crate::terminal::cells::*; +use melib::error::{MeliError, Result}; +use nix::sys::wait::WaitStatus; +use nix::sys::wait::{waitpid, WaitPidFlag}; use std::sync::{Arc, Mutex}; +#[derive(Debug)] pub struct EmbedGrid { cursor: (usize, usize), - terminal_size: (usize, usize), - grid: Arc>, + pub grid: CellBuffer, pub state: State, - stdin: std::fs::File, + pub stdin: std::fs::File, + pub child_pid: nix::unistd::Pid, + pub terminal_size: (usize, usize), + resized: bool, } impl EmbedGrid { - pub fn new(grid: Arc>, stdin: std::fs::File) -> Self { + pub fn new(stdin: std::fs::File, child_pid: nix::unistd::Pid) -> Self { EmbedGrid { - cursor: (1, 1), + cursor: (0, 0), terminal_size: (0, 0), - grid, + grid: CellBuffer::default(), state: State::Normal, stdin, + child_pid, + resized: false, } } pub fn set_terminal_size(&mut self, new_val: (usize, usize)) { self.terminal_size = new_val; + self.grid.resize(new_val.0, new_val.1, Cell::default()); + self.cursor = (0, 0); + nix::sys::signal::kill(self.child_pid, nix::sys::signal::SIGWINCH).unwrap(); + self.resized = true; + } + + pub fn wake_up(&self) { + nix::sys::signal::kill(self.child_pid, nix::sys::signal::SIGCONT).unwrap(); + } + + pub fn stop(&self) { + debug!("stopping"); + nix::sys::signal::kill(debug!(self.child_pid), nix::sys::signal::SIGSTOP).unwrap(); + } + + pub fn is_active(&self) -> Result { + debug!(waitpid(self.child_pid, Some(WaitPidFlag::WNOHANG),)) + .map_err(|e| MeliError::new(e.to_string())) } pub fn process_byte(&mut self, byte: u8) { @@ -32,15 +58,17 @@ impl EmbedGrid { ref mut grid, ref mut state, ref mut stdin, + ref mut resized, + child_pid: _, } = self; macro_rules! increase_cursor_x { () => { if *cursor == *terminal_size { /* do nothing */ - } else if cursor.0 == terminal_size.0 { - cursor.0 = 0; - cursor.1 += 1; + } else if cursor.0 + 1 >= terminal_size.0 { + //cursor.0 = 0; + //cursor.1 += 1; } else { cursor.0 += 1; } @@ -62,6 +90,25 @@ impl EmbedGrid { (b'(', State::ExpectingControlChar) => { *state = State::G0; } + (b'J', State::ExpectingControlChar) => { + // "ESCJ Erase from the cursor to the end of the screen" + debug!("sending {}", EscCode::from((&(*state), byte))); + debug!("erasing from {:?} to {:?}", cursor, terminal_size); + for y in cursor.1..terminal_size.1 { + for x in cursor.0..terminal_size.0 { + grid[(x, y)] = Cell::default(); + } + } + *state = State::Normal; + } + (b'K', State::ExpectingControlChar) => { + // "ESCK Erase from the cursor to the end of the line" + debug!("sending {}", EscCode::from((&(*state), byte))); + for x in cursor.0..terminal_size.0 { + grid[(x, cursor.1)] = Cell::default(); + } + *state = State::Normal; + } (c, State::ExpectingControlChar) => { debug!( "unrecognised: byte is {} and state is {:?}", @@ -89,11 +136,11 @@ impl EmbedGrid { buf.push(c); } (c, State::Osc1(_)) => { - debug!("sending {}", EscCode::from((&(*state), byte))); + debug!("ignoring {}", EscCode::from((&(*state), byte))); *state = State::Normal; } (c, State::Osc2(_, _)) => { - debug!("sending {}", EscCode::from((&(*state), byte))); + debug!("ignoring {}", EscCode::from((&(*state), byte))); *state = State::Normal; } /* END OF OSC */ @@ -101,30 +148,69 @@ impl EmbedGrid { /* ********** */ /* ********** */ /* ********** */ + (b'\r', State::Normal) => { + //debug!("setting cell {:?} char '{}'", cursor, c as char); + debug!("carriage return x-> 0, cursor was: {:?}", cursor); + cursor.0 = 0; + debug!("cursor became: {:?}", cursor); + } + (b'\n', State::Normal) => { + //debug!("setting cell {:?} char '{}'", cursor, c as char); + debug!("newline y-> y+1, cursor was: {:?}", cursor); + if cursor.1 + 1 < terminal_size.1 { + cursor.1 += 1; + } + debug!("cursor became: {:?}", cursor); + } + (b'', State::Normal) => { + debug!("Visual bell ^G, ignoring {:?}", cursor); + } + /* Backspace */ + (0x08, State::Normal) => { + //debug!("setting cell {:?} char '{}'", cursor, c as char); + debug!("backspace x-> x-1, cursor was: {:?}", cursor); + if cursor.0 > 0 { + cursor.0 -= 1; + } + //grid[*cursor].set_ch(' '); + debug!("cursor became: {:?}", cursor); + } (c, State::Normal) => { - grid.lock().unwrap()[*cursor].set_ch(c as char); + grid[*cursor].set_ch(c as char); debug!("setting cell {:?} char '{}'", cursor, c as char); increase_cursor_x!(); } (b'u', State::Csi) => { /* restore cursor */ - debug!("sending {}", EscCode::from((&(*state), byte))); + debug!("ignoring {}", EscCode::from((&(*state), byte))); *state = State::Normal; } (b'm', State::Csi) => { /* Character Attributes (SGR). Ps = 0 -> Normal (default), VT100 */ - debug!("sending {}", EscCode::from((&(*state), byte))); + debug!("ignoring {}", EscCode::from((&(*state), byte))); *state = State::Normal; } (b'H', State::Csi) => { /* move cursor to (1,1) */ - debug!("sending {}", EscCode::from((&(*state), byte)),); debug!("move cursor to (1,1) cursor before: {:?}", *cursor); *cursor = (0, 0); debug!("cursor after: {:?}", *cursor); *state = State::Normal; } + (b'P', State::Csi) => { + /* delete one character */ + debug!("sending {}", EscCode::from((&(*state), byte)),); + grid[*cursor].set_ch(' '); + *state = State::Normal; + } + (b'C', State::Csi) => { + // "ESC[C\t\tCSI Cursor Forward one Time", + debug!("cursor forward one time, cursor was: {:?}", cursor); + cursor.0 = std::cmp::min(cursor.0 + 1, terminal_size.0.saturating_sub(1)); + debug!("cursor became: {:?}", cursor); + *state = State::Normal; + } /* CSI ? stuff */ (c, State::CsiQ(ref mut buf)) if c >= b'0' && c <= b'9' => { buf.push(c); @@ -132,7 +218,7 @@ impl EmbedGrid { (c, State::CsiQ(ref mut buf)) => { // we are already in AlternativeScreen so do not forward this if &buf.as_slice() != &SWITCHALTERNATIVE_1049 { - debug!("sending {}", EscCode::from((&(*state), byte))); + debug!("ignoring {}", EscCode::from((&(*state), byte))); } *state = State::Normal; } @@ -146,71 +232,148 @@ impl EmbedGrid { *state = State::Csi1(buf1); } (b'J', State::Csi) => { - // "ESC[J\t\tCSI Erase from the cursor to the end of the screen [BAD]" - debug!("sending {}", EscCode::from((&(*state), byte))); - let mut grid = grid.lock().unwrap(); - debug!("erasing from {:?} to {:?}", cursor, terminal_size); - for y in cursor.1..terminal_size.1 { - for x in cursor.0..terminal_size.0 { - cursor.0 = x; - grid[(x, y)] = Cell::default(); - } - cursor.1 = y; - } + /* Erase in Display (ED), VT100.*/ + /* Erase Below (default). */ + clear_area( + grid, + ( + ( + 0, + std::cmp::min(cursor.1 + 1, terminal_size.1.saturating_sub(1)), + ), + ( + terminal_size.0.saturating_sub(1), + terminal_size.1.saturating_sub(1), + ), + ), + ); + debug!("{}", EscCode::from((&(*state), byte))); *state = State::Normal; } (b'K', State::Csi) => { - // "ESC[K\t\tCSI Erase from the cursor to the end of the line [BAD]" - debug!("sending {}", EscCode::from((&(*state), byte))); - let mut grid = grid.lock().unwrap(); + /* Erase in Line (ED), VT100.*/ + /* Erase to right (Default) */ + debug!("{}", EscCode::from((&(*state), byte))); for x in cursor.0..terminal_size.0 { - grid[(x, terminal_size.1)] = Cell::default(); + grid[(x, cursor.1)] = Cell::default(); } *state = State::Normal; } (c, State::Csi) => { - debug!("sending {}", EscCode::from((&(*state), byte))); + debug!("ignoring {}", EscCode::from((&(*state), byte))); *state = State::Normal; } - (b'K', State::Csi1(_)) => { - /* Erase in Display (ED), VT100.*/ - debug!("not sending {}", EscCode::from((&(*state), byte))); + (b'K', State::Csi1(buf)) if buf == b"0" => { + /* Erase in Line (ED), VT100.*/ + /* Erase to right (Default) */ + debug!("{}", EscCode::from((&(*state), byte))); + for x in cursor.0..terminal_size.0 { + grid[(x, cursor.1)] = Cell::default(); + } *state = State::Normal; } - (b'J', State::Csi1(_)) => { + (b'K', State::Csi1(buf)) if buf == b"1" => { + /* Erase in Line (ED), VT100.*/ + /* Erase to left (Default) */ + for x in cursor.0..=0 { + grid[(x, cursor.1)] = Cell::default(); + } + debug!("{}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + (b'K', State::Csi1(buf)) if buf == b"2" => { + /* Erase in Line (ED), VT100.*/ + /* Erase all */ + for y in 0..terminal_size.1 { + for x in 0..terminal_size.0 { + grid[(x, y)] = Cell::default(); + } + } + debug!("{}", EscCode::from((&(*state), byte))); + clear_area(grid, ((0, 0), pos_dec(*terminal_size, (1, 1)))); + *state = State::Normal; + } + (b'J', State::Csi1(ref buf)) if buf == b"0" => { /* Erase in Display (ED), VT100.*/ - debug!("not sending {}", EscCode::from((&(*state), byte))); + /* Erase Below (default). */ + clear_area( + grid, + ( + ( + 0, + std::cmp::min(cursor.1 + 1, terminal_size.1.saturating_sub(1)), + ), + ( + terminal_size.0.saturating_sub(1), + terminal_size.1.saturating_sub(1), + ), + ), + ); + debug!("{}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + (b'J', State::Csi1(ref buf)) if buf == b"1" => { + /* Erase in Display (ED), VT100.*/ + /* Erase Above */ + clear_area( + grid, + ( + (0, 0), + ( + terminal_size.0.saturating_sub(1), + cursor.1.saturating_sub(1), + ), + ), + ); + debug!("{}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + (b'J', State::Csi1(ref buf)) if buf == b"2" => { + /* Erase in Display (ED), VT100.*/ + /* Erase All */ + clear_area(grid, ((0, 0), pos_dec(*terminal_size, (1, 1)))); + debug!("{}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + (b'J', State::Csi1(ref buf)) if buf == b"3" => { + /* Erase in Display (ED), VT100.*/ + /* Erase saved lines (What?) */ + debug!("ignoring {}", EscCode::from((&(*state), byte))); *state = State::Normal; } (b't', State::Csi1(buf)) => { - /* Window manipulation, skip it */ + /* Window manipulation */ if buf == b"18" { + debug!("report size of the text area"); + debug!("got {}", EscCode::from((&(*state), byte))); // P s = 1 8 → Report the size of the text area in characters as CSI 8 ; height ; width t - stdin.write_all(&[b'\x1b', b'[', b'8', b';']).unwrap(); + stdin.write_all(b"\x1b[8;").unwrap(); stdin - .write_all((terminal_size.0 + 1).to_string().as_bytes()) + .write_all((terminal_size.1).to_string().as_bytes()) .unwrap(); stdin.write_all(&[b';']).unwrap(); stdin - .write_all((terminal_size.1 + 1).to_string().as_bytes()) + .write_all((terminal_size.0).to_string().as_bytes()) .unwrap(); stdin.write_all(&[b't']).unwrap(); + } else { + debug!("not sending {}", EscCode::from((&(*state), byte))); } - debug!("not sending {}", EscCode::from((&(*state), byte))); *state = State::Normal; } (b'n', State::Csi1(_)) => { /* report cursor position */ + debug!("report cursor position"); debug!("got {}", EscCode::from((&(*state), byte))); stdin.write_all(&[b'\x1b', b'[']).unwrap(); // Ps = 6 ⇒ Report Cursor Position (CPR) [row;column]. //Result is CSI r ; c R stdin - .write_all((cursor.0 + 1).to_string().as_bytes()) + .write_all((cursor.1 + 1).to_string().as_bytes()) .unwrap(); stdin.write_all(&[b';']).unwrap(); stdin - .write_all((cursor.1 + 1).to_string().as_bytes()) + .write_all((cursor.0 + 1).to_string().as_bytes()) .unwrap(); stdin.write_all(&[b'R']).unwrap(); *state = State::Normal; @@ -262,7 +425,7 @@ impl EmbedGrid { ); if offset + cursor.1 < terminal_size.1 { cursor.1 += offset; - cursor.0 = 0; + //cursor.0 = 0; } debug!("cursor became: {:?}", cursor); *state = State::Normal; @@ -273,14 +436,19 @@ impl EmbedGrid { .parse::() .unwrap(); debug!("cursor absolute {}, cursor was: {:?}", new_col, cursor); - if new_col < terminal_size.0 { - cursor.0 = new_col; + if new_col < terminal_size.0 + 1 { + cursor.0 = new_col.saturating_sub(1); + } else { + debug!( + "error: new_cal = {} > terminal.size.0 = {}\nterminal_size = {:?}", + new_col, terminal_size.0, terminal_size + ); } debug!("cursor became: {:?}", cursor); *state = State::Normal; } (b'C', State::Csi1(buf)) => { - // "ESC[{buf}F\t\tCSI Cursor Preceding Line {buf} Times", + // "ESC[{buf}C\t\tCSI Cursor Preceding Line {buf} Times", let offset = unsafe { std::str::from_utf8_unchecked(buf) } .parse::() .unwrap(); @@ -288,13 +456,41 @@ impl EmbedGrid { "cursor preceding {} times, cursor was: {:?}", offset, cursor ); - if cursor.1 < offset + terminal_size.1 { + if cursor.1 >= offset { cursor.1 -= offset; - cursor.0 = 0; + //cursor.0 = 0; } debug!("cursor became: {:?}", cursor); *state = State::Normal; } + (b'P', State::Csi1(buf)) => { + // "ESC[{buf}P\t\tCSI Delete {buf} characters, default = 1", + let offset = unsafe { std::str::from_utf8_unchecked(buf) } + .parse::() + .unwrap(); + debug!( + "Delete {} Character(s) with cursor at {:?} ", + offset, cursor + ); + for x in (cursor.0 - std::cmp::min(offset, cursor.0))..cursor.0 { + grid[(x, cursor.1)].set_ch(' '); + } + debug!("cursor became: {:?}", cursor); + *state = State::Normal; + } + /* CSI Pm d Line Position Absolute [row] (default = [1,column]) (VPA). */ + (b'd', State::Csi1(buf)) => { + let row = unsafe { std::str::from_utf8_unchecked(buf) } + .parse::() + .unwrap(); + debug!( + "Line position absolute row {} with cursor at {:?}", + row, cursor + ); + cursor.1 = std::cmp::min(row.saturating_sub(1), terminal_size.1.saturating_sub(1)); + debug!("cursor became: {:?}", cursor); + *state = State::Normal; + } (b';', State::Csi1(ref mut buf1_p)) => { let buf1 = std::mem::replace(buf1_p, Vec::new()); let buf2 = Vec::new(); @@ -304,7 +500,7 @@ impl EmbedGrid { buf.push(c); } (c, State::Csi1(ref buf)) => { - debug!("sending {}", EscCode::from((&(*state), byte))); + debug!("ignoring {}", EscCode::from((&(*state), byte))); *state = State::Normal; } (b';', State::Csi2(ref mut buf1_p, ref mut buf2_p)) => { @@ -314,29 +510,36 @@ impl EmbedGrid { *state = State::Csi3(buf1, buf2, buf3); } (b'n', State::Csi2(_, _)) => { + debug!("ignoring {}", EscCode::from((&(*state), byte))); // Report Cursor Position, skip it *state = State::Normal; } (b't', State::Csi2(_, _)) => { + debug!("ignoring {}", EscCode::from((&(*state), byte))); // Window manipulation, skip it *state = State::Normal; } - (b'H', State::Csi2(ref x, ref y)) => { + (b'H', State::Csi2(ref y, ref x)) => { //Cursor Position [row;column] (default = [1,1]) (CUP). let orig_x = unsafe { std::str::from_utf8_unchecked(x) } .parse::() - .unwrap(); + .unwrap_or(1); let orig_y = unsafe { std::str::from_utf8_unchecked(y) } .parse::() - .unwrap(); + .unwrap_or(1); debug!("sending {}", EscCode::from((&(*state), byte)),); debug!( "cursor set to ({},{}), cursor was: {:?}", orig_x, orig_y, cursor ); - if orig_x - 1 <= terminal_size.0 && orig_y - 1 <= terminal_size.1 { + if orig_x - 1 < terminal_size.0 && orig_y - 1 < terminal_size.1 { cursor.0 = orig_x - 1; cursor.1 = orig_y - 1; + } else { + debug!( + "[error] terminal_size = {:?}, cursor = {:?} but given [{},{}]", + terminal_size, cursor, orig_x, orig_y + ); } debug!("cursor became: {:?}", cursor); *state = State::Normal; @@ -345,10 +548,11 @@ impl EmbedGrid { buf.push(c); } (c, State::Csi2(ref buf1, ref buf2)) => { - debug!("sending {}", EscCode::from((&(*state), byte))); + debug!("ignoring {}", EscCode::from((&(*state), byte))); *state = State::Normal; } (b't', State::Csi3(_, _, _)) => { + debug!("ignoring {}", EscCode::from((&(*state), byte))); // Window manipulation, skip it *state = State::Normal; } @@ -356,8 +560,8 @@ impl EmbedGrid { (c, State::Csi3(_, _, ref mut buf)) if c >= b'0' && c <= b'9' => { buf.push(c); } - (c, State::Csi3(ref buf1, ref buf2, ref buf3)) => { - debug!("sending {}", EscCode::from((&(*state), byte))); + (c, State::Csi3(_, _, _)) => { + debug!("ignoring {}", EscCode::from((&(*state), byte))); *state = State::Normal; } /* other stuff */ @@ -365,7 +569,7 @@ impl EmbedGrid { /* ******************* */ /* ******************* */ (c, State::G0) => { - debug!("sending {}", EscCode::from((&(*state), byte))); + debug!("ignoring {}", EscCode::from((&(*state), byte))); *state = State::Normal; } (b, s) => { diff --git a/ui/src/terminal/keys.rs b/ui/src/terminal/keys.rs index ebf4cfe22..a349d9fed 100644 --- a/ui/src/terminal/keys.rs +++ b/ui/src/terminal/keys.rs @@ -139,8 +139,17 @@ impl PartialEq for &Key { enum InputMode { Normal, Paste, + PasteRaw(Vec), } +pub enum InputCommand { + Kill, + /// Send Raw bytes as well + Raw, + NoRaw, +} + +use termion::input::TermReadEventsAndRaw; /* * If we fork (for example start $EDITOR) we want the input-thread to stop reading from stdin. The * best way I came up with right now is to send a signal to the thread that is read in the first @@ -150,22 +159,21 @@ enum InputMode { * The main loop uses try_wait_on_child() to check if child has exited. */ pub fn get_events( - stdin: io::Stdin, mut closure: impl FnMut(Key), - mut exit: impl FnMut(), - rx: &Receiver, -) { + closure_raw: impl FnMut((Key, Vec)), + rx: &Receiver, +) -> ! { + let stdin = std::io::stdin(); let mut input_mode = InputMode::Normal; let mut paste_buf = String::with_capacity(256); for c in stdin.events() { select! { default => {}, - recv(rx) -> val => { - if let Ok(true) = val { - exit(); - return; - } else { - return; + recv(rx) -> cmd => { + match cmd.unwrap() { + InputCommand::Kill => std::process::exit(0), + InputCommand::Raw => get_events_raw(closure, closure_raw, rx), + InputCommand::NoRaw => unreachable!(), } } }; @@ -189,6 +197,59 @@ pub fn get_events( _ => {} // Mouse events or errors. } } + std::process::exit(0) +} + +pub fn get_events_raw( + closure_nonraw: impl FnMut(Key), + mut closure: impl FnMut((Key, Vec)), + rx: &Receiver, +) -> ! { + let stdin = std::io::stdin(); + let mut input_mode = InputMode::Normal; + let mut paste_buf = String::with_capacity(256); + for c in stdin.events_and_raw() { + select! { + default => {}, + recv(rx) -> cmd => { + match cmd.unwrap() { + InputCommand::Kill => std::process::exit(0), + InputCommand::NoRaw => get_events(closure_nonraw, closure, rx), + InputCommand::Raw => unreachable!(), + } + } + }; + + match (c, &mut input_mode) { + (Ok((TermionEvent::Key(k), bytes)), InputMode::Normal) => { + closure((Key::from(k), bytes)); + } + ( + Ok((TermionEvent::Key(TermionKey::Char(k)), ref mut bytes)), + InputMode::PasteRaw(ref mut buf), + ) => { + paste_buf.push(k); + let bytes = std::mem::replace(bytes, Vec::new()); + buf.extend(bytes.into_iter()); + } + (Ok((TermionEvent::Unsupported(ref k), _)), _) + if k.as_slice() == BRACKET_PASTE_START => + { + input_mode = InputMode::PasteRaw(Vec::new()); + } + (Ok((TermionEvent::Unsupported(ref k), _)), InputMode::PasteRaw(ref mut buf)) + if k.as_slice() == BRACKET_PASTE_END => + { + let buf = std::mem::replace(buf, Vec::new()); + input_mode = InputMode::Normal; + let ret = Key::from(&paste_buf); + paste_buf.clear(); + closure((ret, buf)); + } + _ => {} // Mouse events or errors. + } + } + std::process::exit(0) } const FIELDS: &[&str] = &[]; diff --git a/ui/src/types.rs b/ui/src/types.rs index 57af6f729..4746a5330 100644 --- a/ui/src/types.rs +++ b/ui/src/types.rs @@ -48,6 +48,8 @@ pub enum ThreadEvent { ThreadJoin(thread::ThreadId), /// User input. Input(Key), + /// User input and input as raw bytes. + InputRaw((Key, Vec)), /// A watched folder has been refreshed. RefreshMailbox(Box), UIEvent(UIEvent), @@ -84,9 +86,11 @@ pub enum UIEvent { Input(Key), ExInput(Key), InsertInput(Key), - EmbedInput(Key), + EmbedInput((Key, Vec)), RefreshMailbox((usize, FolderHash)), //view has changed to FolderHash mailbox - //Quit? + ChildStatusExited(nix::unistd::Pid, i32), + ChildStatusStopped(nix::unistd::Pid), + ChildStatusContinued(nix::unistd::Pid), Resize, /// Force redraw. Fork(ForkType),