From 99da9a35b6c69e5b17171c4671b03055f3e98c3d Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Tue, 5 Nov 2019 08:32:27 +0200 Subject: [PATCH] Add embed pty support Emulate a terminal within meli. In the next commit it will be used to embed an editor in the composing tab. This is a non-complete xterm emulation that has some bugs. --- Cargo.lock | 14 + src/bin.rs | 8 + ui/Cargo.toml | 1 + ui/src/state.rs | 60 ++- ui/src/terminal.rs | 1 + ui/src/terminal/embed.rs | 319 ++++++++++++ ui/src/terminal/embed/grid.rs | 889 ++++++++++++++++++++++++++++++++++ ui/src/terminal/keys.rs | 87 +++- ui/src/types.rs | 11 +- 9 files changed, 1361 insertions(+), 29 deletions(-) create mode 100644 ui/src/terminal/embed.rs create mode 100644 ui/src/terminal/embed/grid.rs diff --git a/Cargo.lock b/Cargo.lock index 8f998c42..e73e33c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -645,6 +645,18 @@ dependencies = [ "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "nix" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)", + "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "nodrop" version = "0.1.13" @@ -1181,6 +1193,7 @@ dependencies = [ "linkify 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "melib 0.3.2", "mime_apps 0.2.0 (git+https://git.meli.delivery/meli/mime_apps)", + "nix 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "nom 3.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "notify 4.0.12 (registry+https://github.com/rust-lang/crates.io-index)", "notify-rust 3.6.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1358,6 +1371,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b2df1a4c22fd44a62147fd8f13dd0f95c9d8ca7b2610299b2a2f9cf8964274e" "checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" "checksum nix 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce" +"checksum nix 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229" "checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" "checksum nom 3.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05aec50c70fd288702bcd93284a8444607f3292dbdf2a30de5ea5dcdbe72287b" "checksum notify 4.0.12 (registry+https://github.com/rust-lang/crates.io-index)" = "3572d71f13ea8ed41867accd971fd564aa75934cf7a1fae03ddb8c74a8a49943" diff --git a/src/bin.rs b/src/bin.rs index 66770f5f..158e79b9 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,11 +305,17 @@ fn main() -> std::result::Result<(), std::io::Error> { }, } }, + UIMode::Embed => state.redraw(), + 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/Cargo.toml b/ui/Cargo.toml index d0a616ef..307df1bc 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -25,6 +25,7 @@ uuid = { version = "0.7.4", features = ["serde", "v4"] } unicode-segmentation = "1.2.1" # >:c text_processing = { path = "../text_processing", version = "*" } libc = {version = "0.2.59", features = ["extra_traits",]} +nix = "0.15.0" [features] default = [] diff --git a/ui/src/state.rs b/ui/src/state.rs index a20480ee..d6cfb42a 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()); } @@ -569,25 +585,35 @@ impl State { self.parse_command(&cmd); return; } + UIEvent::Fork(ForkType::Finished) => { + /* + * Fork has finished in the past. + * We're back in the AlternateScreen, but the cursor is reset to Shown, so fix + * it. + write!(self.stdout(), "{}", cursor::Hide,).unwrap(); + self.flush(); + */ + self.switch_to_main_screen(); + self.switch_to_alternate_screen(); + self.context.restore_input(); + return; + } UIEvent::Fork(child) => { self.mode = UIMode::Fork; self.child = Some(child); - if let Some(ForkType::Finished) = self.child { - /* - * Fork has finished in the past. - * We're back in the AlternateScreen, but the cursor is reset to Shown, so fix - * it. - */ - write!(self.stdout(), "{}", cursor::Hide,).unwrap(); - self.flush(); - } 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.rs b/ui/src/terminal.rs index 16a118d1..13b956b3 100644 --- a/ui/src/terminal.rs +++ b/ui/src/terminal.rs @@ -29,6 +29,7 @@ mod position; mod cells; #[macro_use] mod keys; +pub mod embed; mod text_editing; pub use self::cells::*; pub use self::keys::*; diff --git a/ui/src/terminal/embed.rs b/ui/src/terminal/embed.rs new file mode 100644 index 00000000..87cd3fd0 --- /dev/null +++ b/ui/src/terminal/embed.rs @@ -0,0 +1,319 @@ +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, wait::waitpid}; +use nix::unistd::{dup2, fork, ForkResult}; +use std::ffi::CString; +use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd}; + +mod grid; + +pub use grid::EmbedGrid; + +// ioctl command to set window size of pty: +use libc::TIOCSWINSZ; +use std::path::Path; + +use std::convert::TryFrom; +use std::io::Read; +use std::io::Write; +use std::sync::{Arc, Mutex}; + +// Macro generated function that calls ioctl to set window size of slave pty end +ioctl_write_ptr_bad!(set_window_size, TIOCSWINSZ, Winsize); + +pub fn create_pty(area: Area, command: String) -> nix::Result>> { + // 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) }?; + + // Try to open the slave + //let _slave_fd = open(Path::new(&slave_name), OFlag::O_RDWR, Mode::empty())?; + { + let winsize = Winsize { + ws_row: ::try_from(height!(area)).unwrap(), + ws_col: ::try_from(width!(area)).unwrap(), + ws_xpixel: 0, + ws_ypixel: 0, + }; + + let master_fd = master_fd.clone().into_raw_fd(); + unsafe { set_window_size(master_fd, &winsize).unwrap() }; + } + + let child_pid = match fork() { + Ok(ForkResult::Child) => { + /* Open slave end for pseudoterminal */ + let slave_fd = open(Path::new(&slave_name), OFlag::O_RDWR, stat::Mode::empty())?; + + let child_pid = match fork() { + Ok(ForkResult::Child) => { + // 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(); + 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 }) => child, + Err(e) => panic!(e), + }; + + let stdin = unsafe { std::fs::File::from_raw_fd(master_fd.clone().into_raw_fd()) }; + 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(); + + 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(); + 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)] +pub enum State { + ExpectingControlChar, + G0, // Designate G0 Character Set + Osc1(Vec), //ESC ] Operating System Command (OSC is 0x9d). + Osc2(Vec, Vec), + Csi, // ESC [ Control Sequence Introducer (CSI is 0x9b). + Csi1(Vec), + Csi2(Vec, Vec), + Csi3(Vec, Vec, Vec), + CsiQ(Vec), + Normal, +} + +/* Used for debugging */ +struct EscCode<'a>(&'a State, u8); + +impl<'a> From<(&'a mut State, u8)> for EscCode<'a> { + fn from(val: (&mut State, u8)) -> EscCode { + let (s, b) = val; + EscCode(s, b) + } +} + +impl<'a> From<(&'a State, u8)> for EscCode<'a> { + fn from(val: (&State, u8)) -> EscCode { + let (s, b) = val; + EscCode(s, b) + } +} + +impl std::fmt::Display for EscCode<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use State::*; + macro_rules! unsafestr { + ($buf:ident) => { + unsafe { std::str::from_utf8_unchecked($buf) } + }; + } + 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) + } + EscCode(Osc2(ref buf1, ref buf2), c) => write!( + f, + "ESC]{};{}{}\t\tOSC [UNKNOWN]", + unsafestr!(buf1), + unsafestr!(buf2), + *c as char + ), + EscCode(Csi, b'm') => write!( + f, + "ESC[m\t\tCSI Character Attributes | Set Attr and Color to Normal (default)" + ), + EscCode(Csi, b'K') => write!( + f, + "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" + ), + 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, + "ESC[{}m\t\tCSI Character Attributes | Set fg, bg color", + unsafestr!(buf) + ), + EscCode(Csi1(ref buf), b'n') => write!( + f, + "ESC[{}n\t\tCSI Device Status Report (DSR)| Report Cursor Position", + unsafestr!(buf) + ), + EscCode(Csi1(ref buf), b't') if buf == b"18" => write!( + f, + "ESC[18t\t\tReport the size of the text area in characters", + ), + EscCode(Csi1(ref buf), b't') => write!( + f, + "ESC[{buf}t\t\tWindow manipulation, skipped", + buf = unsafestr!(buf) + ), + EscCode(Csi1(ref buf), b'B') => write!( + f, + "ESC[{buf}B\t\tCSI Cursor Down {buf} Times", + buf = unsafestr!(buf) + ), + EscCode(Csi1(ref buf), b'C') => write!( + f, + "ESC[{buf}C\t\tCSI Cursor Forward {buf} Times", + buf = unsafestr!(buf) + ), + EscCode(Csi1(ref buf), b'D') => write!( + f, + "ESC[{buf}D\t\tCSI Cursor Backward {buf} Times", + buf = unsafestr!(buf) + ), + EscCode(Csi1(ref buf), b'E') => write!( + f, + "ESC[{buf}E\t\tCSI Cursor Next Line {buf} Times", + buf = unsafestr!(buf) + ), + EscCode(Csi1(ref buf), b'F') => write!( + f, + "ESC[{buf}F\t\tCSI Cursor Preceding Line {buf} Times", + buf = unsafestr!(buf) + ), + EscCode(Csi1(ref buf), b'G') => write!( + f, + "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", + unsafestr!(buf1), + unsafestr!(buf2), + *c as char + ), + EscCode(Csi3(ref buf1, ref buf2, ref buf3), b'm') => write!( + f, + "ESC[{};{};{}m\t\tCSI Character Attributes | Set fg, bg color", + unsafestr!(buf1), + unsafestr!(buf2), + unsafestr!(buf3), + ), + EscCode(Csi3(ref buf1, ref buf2, ref buf3), c) => write!( + f, + "ESC[{};{};{}{}\t\tCSI [UNKNOWN]", + unsafestr!(buf1), + unsafestr!(buf2), + unsafestr!(buf3), + *c as char + ), + EscCode(CsiQ(ref buf), b's') => write!( + f, + "ESC[?{}r\t\tCSI Save DEC Private Mode Values", + unsafestr!(buf) + ), + EscCode(CsiQ(ref buf), b'r') => write!( + f, + "ESC[?{}r\t\tCSI Restore DEC Private Mode Values", + unsafestr!(buf) + ), + 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"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", + ), + EscCode(CsiQ(ref buf), c) => { + write!(f, "ESC[?{}{}\t\tCSI [UNKNOWN]", unsafestr!(buf), *c as char) + } + EscCode(unknown, c) => { + write!(f, "{:?}{} [UNKNOWN]", unknown, c) + } + } + } +} diff --git a/ui/src/terminal/embed/grid.rs b/ui/src/terminal/embed/grid.rs new file mode 100644 index 00000000..a30ed232 --- /dev/null +++ b/ui/src/terminal/embed/grid.rs @@ -0,0 +1,889 @@ +use super::*; +use crate::terminal::cells::*; +use melib::error::{MeliError, Result}; +use nix::sys::wait::WaitStatus; +use nix::sys::wait::{waitpid, WaitPidFlag}; +/** + * `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. + **/ + +/// In a scroll region up and down cursor movements shift the region vertically. The new lines are +/// empty. +#[derive(Debug)] +struct ScrollRegion { + top: usize, + bottom: usize, +} + +#[derive(Debug)] +pub struct EmbedGrid { + cursor: (usize, usize), + /// [top;bottom] + scroll_region: ScrollRegion, + pub grid: CellBuffer, + pub state: State, + pub stdin: std::fs::File, + /// Pid of the embed process + pub child_pid: nix::unistd::Pid, + /// (width, height) + pub terminal_size: (usize, usize), + fg_color: Color, + bg_color: Color, + /// 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, + + show_cursor: bool, + /// Store state in case a multi-byte character is encountered + codepoints: CodepointBuf, +} + +#[derive(Debug, PartialEq)] +enum CodepointBuf { + None, + TwoCodepoints(Vec), + ThreeCodepoints(Vec), + FourCodepoints(Vec), +} + +impl EmbedGrid { + pub fn new(stdin: std::fs::File, child_pid: nix::unistd::Pid) -> Self { + EmbedGrid { + cursor: (0, 0), + scroll_region: ScrollRegion { top: 0, bottom: 0 }, + terminal_size: (0, 0), + grid: CellBuffer::default(), + state: State::Normal, + stdin, + child_pid, + fg_color: Color::Default, + bg_color: Color::Default, + prev_fg_color: None, + prev_bg_color: None, + show_cursor: true, + codepoints: CodepointBuf::None, + } + } + + pub fn set_terminal_size(&mut self, new_val: (usize, usize)) { + if new_val == self.terminal_size { + return; + } + debug!("resizing to {:?}", new_val); + if self.scroll_region.top == 0 && self.scroll_region.bottom == self.terminal_size.1 { + self.scroll_region.bottom = new_val.1; + } + self.terminal_size = new_val; + self.grid.resize(new_val.0, new_val.1, Cell::default()); + self.grid.clear(Cell::default()); + self.cursor = (0, 0); + let winsize = Winsize { + ws_row: ::try_from(new_val.1).unwrap(), + ws_col: ::try_from(new_val.0).unwrap(), + ws_xpixel: 0, + ws_ypixel: 0, + }; + + let master_fd = self.stdin.as_raw_fd(); + unsafe { set_window_size(master_fd, &winsize).unwrap() }; + nix::sys::signal::kill(self.child_pid, nix::sys::signal::SIGWINCH).unwrap(); + } + + 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) { + let EmbedGrid { + ref mut cursor, + ref mut scroll_region, + ref terminal_size, + ref mut grid, + ref mut state, + ref mut stdin, + ref mut fg_color, + ref mut bg_color, + ref mut prev_fg_color, + ref mut prev_bg_color, + ref mut codepoints, + ref mut show_cursor, + child_pid: _, + } = self; + + macro_rules! increase_cursor_x { + () => { + if cursor.0 + 1 < terminal_size.0 { + cursor.0 += 1; + } + }; + } + + macro_rules! cursor_x { + () => {{ + if cursor.0 >= terminal_size.0 { + cursor.0 = terminal_size.0.saturating_sub(1); + } + cursor.0 + }}; + } + macro_rules! cursor_y { + () => { + std::cmp::min( + cursor.1 + scroll_region.top, + terminal_size.1.saturating_sub(1), + ) + }; + } + macro_rules! cursor_val { + () => { + (cursor_x!(), cursor_y!()) + }; + } + + let mut state = state; + match (byte, &mut state) { + (b'\x1b', State::Normal) => { + *state = State::ExpectingControlChar; + } + (b']', State::ExpectingControlChar) => { + let buf1 = Vec::new(); + *state = State::Osc1(buf1); + } + (b'[', State::ExpectingControlChar) => { + *state = State::Csi; + } + (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; + } + (_, State::ExpectingControlChar) => { + debug!( + "unrecognised: byte is {} and state is {:?}", + byte as char, state + ); + *state = State::Normal; + } + (b'?', State::Csi) => { + let buf1 = Vec::new(); + *state = State::CsiQ(buf1); + } + /* OSC stuff */ + (c, State::Osc1(ref mut buf)) if (c >= b'0' && c <= b'9') || c == b'?' => { + buf.push(c); + } + (b';', State::Osc1(ref mut buf1_p)) => { + let buf1 = std::mem::replace(buf1_p, Vec::new()); + let buf2 = Vec::new(); + *state = State::Osc2(buf1, buf2); + } + (c, State::Osc2(_, ref mut buf)) if (c >= b'0' && c <= b'9') || c == b'?' => { + buf.push(c); + } + (_, State::Osc1(_)) => { + debug!("ignoring unknown code {}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + (_, State::Osc2(_, _)) => { + debug!("ignoring unknown code {}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + /* Normal */ + (b'\r', State::Normal) => { + 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); + } + (0x08, State::Normal) => { + /* Backspace */ + debug!("backspace x-> x-1, cursor was: {:?}", cursor); + if cursor.0 > 0 { + cursor.0 -= 1; + } + debug!("cursor became: {:?}", cursor); + } + (c, State::Normal) => { + /* Character to be printed. */ + if *codepoints == CodepointBuf::None && c & 0x80 == 0 { + /* This is a one byte char */ + grid[cursor_val!()].set_ch(c as char); + } else { + match codepoints { + CodepointBuf::None if c & 0b1110_0000 == 0b1100_0000 => { + *codepoints = CodepointBuf::TwoCodepoints(vec![c]); + } + CodepointBuf::None if c & 0b1111_0000 == 0b1110_0000 => { + *codepoints = CodepointBuf::ThreeCodepoints(vec![c]); + } + CodepointBuf::None if c & 0b1111_1000 == 0b1111_0000 => { + *codepoints = CodepointBuf::FourCodepoints(vec![c]); + } + CodepointBuf::TwoCodepoints(buf) => { + grid[cursor_val!()].set_ch( + unsafe { std::str::from_utf8_unchecked(&[buf[0], c]) } + .chars() + .next() + .unwrap(), + ); + *codepoints = CodepointBuf::None; + } + CodepointBuf::ThreeCodepoints(buf) if buf.len() == 2 => { + grid[cursor_val!()].set_ch( + unsafe { std::str::from_utf8_unchecked(&[buf[0], buf[1], c]) } + .chars() + .next() + .unwrap(), + ); + *codepoints = CodepointBuf::None; + } + CodepointBuf::ThreeCodepoints(buf) => { + buf.push(c); + return; + } + CodepointBuf::FourCodepoints(buf) if buf.len() == 3 => { + grid[cursor_val!()].set_ch( + unsafe { + std::str::from_utf8_unchecked(&[buf[0], buf[1], buf[2], c]) + } + .chars() + .next() + .unwrap(), + ); + *codepoints = CodepointBuf::None; + } + CodepointBuf::FourCodepoints(buf) => { + buf.push(c); + return; + } + _ => { + debug!( + "invalid utf8 sequence: codepoints = {:?} and c={}", + codepoints, c + ); + *codepoints = CodepointBuf::None; + } + } + } + + grid[cursor_val!()].set_fg(*fg_color); + grid[cursor_val!()].set_bg(*bg_color); + increase_cursor_x!(); + } + (b'u', State::Csi) => { + /* restore cursor */ + debug!("restore cursor {}", EscCode::from((&(*state), byte))); + *show_cursor = true; + *state = State::Normal; + } + (b'm', State::Csi) => { + /* Reset character Attributes (SGR). Ps = 0 -> Normal (default), VT100 */ + debug!("{}", EscCode::from((&(*state), byte))); + *fg_color = Color::Default; + *bg_color = Color::Default; + grid[cursor_val!()].set_fg(Color::Default); + grid[cursor_val!()].set_bg(Color::Default); + *state = State::Normal; + } + (b'H', State::Csi) => { + /* move cursor to (1,1) */ + debug!("{}", 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!("{}", EscCode::from((&(*state), byte)),); + grid[cursor_val!()].set_ch(' '); + *state = State::Normal; + } + (b'C', State::Csi) => { + // ESC[C CSI 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); + } + (b'h', State::CsiQ(ref buf)) => { + match buf.as_slice() { + b"25" => { + *show_cursor = true; + *prev_fg_color = Some(grid[cursor_val!()].fg()); + *prev_bg_color = Some(grid[cursor_val!()].bg()); + grid[cursor_val!()].set_fg(Color::Black); + grid[cursor_val!()].set_bg(Color::White); + } + _ => {} + } + + debug!("{}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + (b'l', State::CsiQ(ref mut buf)) => { + match buf.as_slice() { + b"25" => { + *show_cursor = false; + if let Some(fg_color) = prev_fg_color.take() { + grid[cursor_val!()].set_fg(fg_color); + } else { + grid[cursor_val!()].set_fg(*fg_color); + } + if let Some(bg_color) = prev_bg_color.take() { + grid[cursor_val!()].set_bg(bg_color); + } else { + grid[cursor_val!()].set_bg(*bg_color); + } + } + _ => {} + } + debug!("{}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + (_, State::CsiQ(_)) => { + debug!("ignoring unknown code {}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + /* END OF CSI ? stuff */ + (c, State::Csi) if c >= b'0' && c <= b'9' => { + let mut buf1 = Vec::new(); + buf1.push(c); + *state = State::Csi1(buf1); + } + (b'J', State::Csi) => { + /* Erase in Display (ED), VT100.*/ + /* Erase Below (default). */ + clear_area( + grid, + ( + ( + 0, + std::cmp::min( + cursor.1 + 1 + scroll_region.top, + 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) => { + /* 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 + scroll_region.top)] = Cell::default(); + } + *state = State::Normal; + } + (b'M', State::Csi) => { + /* Delete line */ + debug!("{}", EscCode::from((&(*state), byte))); + for x in 0..terminal_size.0 { + grid[(x, cursor.1 + scroll_region.top)] = Cell::default(); + } + *state = State::Normal; + } + (b'A', State::Csi) => { + // Move cursor up 1 line + debug!("cursor up 1 times, cursor was: {:?}", cursor); + if cursor.1 > 0 { + cursor.1 -= 1; + } else { + debug!("cursor.1 == 0"); + } + debug!("cursor became: {:?}", cursor); + *state = State::Normal; + } + (b'r', State::Csi) => { + // Set scrolling region to default (size of window) + scroll_region.top = 0; + scroll_region.bottom = terminal_size.1.saturating_sub(1); + *cursor = (0, 0); + *state = State::Normal; + } + (_, State::Csi) => { + debug!("ignoring unknown code {}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + (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 + scroll_region.top)] = Cell::default(); + } + *state = State::Normal; + } + (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 + scroll_region.top)] = 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.*/ + /* Erase Below (default). */ + clear_area( + grid, + ( + ( + 0, + std::cmp::min( + cursor.1 + 1 + scroll_region.top, + 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) + scroll_region.top, + ), + ), + ); + 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 unknown code {}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + (b'X', State::Csi1(ref buf)) => { + /* Erase Ps Character(s) (default = 1) (ECH)..*/ + let ps = unsafe { std::str::from_utf8_unchecked(buf) } + .parse::() + .unwrap(); + + let mut ctr = 0; + let (mut cur_x, mut cur_y) = cursor_val!(); + while ctr < ps { + if cur_x >= terminal_size.0 { + cur_y += 1; + cur_x = 0; + if cur_y >= terminal_size.1 { + break; + } + } + grid[(cur_x, cur_y)] = Cell::default(); + cur_x += 1; + ctr += 1; + } + debug!("Erased {} Character(s)", ps); + *state = State::Normal; + } + (b't', State::Csi1(buf)) => { + /* Window manipulation */ + if buf == b"18" { + // Ps = 18 → Report the size of the text area in characters as CSI 8 ; height ; width t + debug!("report size of the text area"); + debug!("got {}", EscCode::from((&(*state), byte))); + stdin.write_all(b"\x1b[8;").unwrap(); + stdin + .write_all((terminal_size.1).to_string().as_bytes()) + .unwrap(); + stdin.write_all(&[b';']).unwrap(); + stdin + .write_all((terminal_size.0).to_string().as_bytes()) + .unwrap(); + stdin.write_all(&[b't']).unwrap(); + } else { + debug!("ignoring unknown code {}", EscCode::from((&(*state), byte))); + } + *state = State::Normal; + } + (b'n', State::Csi1(_)) => { + // Ps = 6 ⇒ Report Cursor Position (CPR) [row;column]. + // Result is CSI r ; c R + debug!("report cursor position"); + debug!("got {}", EscCode::from((&(*state), byte))); + stdin.write_all(&[b'\x1b', b'[']).unwrap(); + stdin + .write_all((cursor.1 + 1).to_string().as_bytes()) + .unwrap(); + stdin.write_all(&[b';']).unwrap(); + stdin + .write_all((cursor.0 + 1).to_string().as_bytes()) + .unwrap(); + stdin.write_all(&[b'R']).unwrap(); + *state = State::Normal; + } + (b'A', State::Csi1(buf)) => { + // Move cursor up n lines + let offset = unsafe { std::str::from_utf8_unchecked(buf) } + .parse::() + .unwrap(); + debug!("cursor up {} times, cursor was: {:?}", offset, cursor); + if cursor.1 == scroll_region.top { + for y in scroll_region.top..scroll_region.bottom { + for x in 0..terminal_size.1 { + grid[(x, y)] = grid[(x, y + 1)]; + } + } + for x in 0..terminal_size.1 { + grid[(x, scroll_region.bottom)] = Cell::default(); + } + } else if cursor.1 >= offset { + cursor.1 -= offset; + } else { + debug!("offset > cursor.1"); + } + debug!("cursor became: {:?}", cursor); + *state = State::Normal; + } + (b'B', State::Csi1(buf)) => { + // ESC[{buf}B CSI Cursor Down {buf} Times + let offset = unsafe { std::str::from_utf8_unchecked(buf) } + .parse::() + .unwrap(); + debug!("cursor down {} times, cursor was: {:?}", offset, cursor); + if offset + cursor.1 < terminal_size.1 { + cursor.1 += offset; + } + if scroll_region.top + cursor.1 >= terminal_size.1 { + cursor.1 = terminal_size.1.saturating_sub(1); + } + debug!("cursor became: {:?}", cursor); + *state = State::Normal; + } + (b'D', State::Csi1(buf)) => { + // ESC[{buf}D CSI Cursor Backward {buf} Times + let offset = unsafe { std::str::from_utf8_unchecked(buf) } + .parse::() + .unwrap(); + debug!("cursor backward {} times, cursor was: {:?}", offset, cursor); + if offset + cursor.0 < terminal_size.0 { + cursor.0 += offset; + } + debug!("cursor became: {:?}", cursor); + *state = State::Normal; + } + (b'E', State::Csi1(buf)) => { + // ESC[{buf}E CSI Cursor Next Line {buf} Times + let offset = unsafe { std::str::from_utf8_unchecked(buf) } + .parse::() + .unwrap(); + debug!( + "cursor next line {} times, cursor was: {:?}", + offset, cursor + ); + if offset + cursor.1 < terminal_size.1 { + cursor.1 += offset; + } + if scroll_region.top + cursor.1 >= terminal_size.1 { + cursor.1 = terminal_size.1.saturating_sub(1); + } + debug!("cursor became: {:?}", cursor); + *state = State::Normal; + } + (b'G', State::Csi1(buf)) => { + // ESC[{buf}G Cursor Character Absolute [column={buf}] (default = [row,1]) + let new_col = unsafe { std::str::from_utf8_unchecked(buf) } + .parse::() + .unwrap(); + debug!("cursor absolute {}, cursor was: {:?}", new_col, cursor); + if new_col < terminal_size.0 { + 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}C CSI Cursor Preceding Line {buf} Times + let offset = unsafe { std::str::from_utf8_unchecked(buf) } + .parse::() + .unwrap(); + debug!( + "cursor preceding {} times, cursor was: {:?}", + offset, cursor + ); + if cursor.1 >= offset { + cursor.1 -= offset; + } + if scroll_region.top + cursor.1 >= terminal_size.1 { + cursor.1 = terminal_size.1.saturating_sub(1); + } + debug!("cursor became: {:?}", cursor); + *state = State::Normal; + } + (b'P', State::Csi1(buf)) => { + // ESC[{buf}P CSI 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 + scroll_region.top)].set_ch(' '); + } + debug!("cursor became: {:?}", cursor); + *state = State::Normal; + } + (b'd', State::Csi1(buf)) => { + /* CSI Pm d Line Position Absolute [row] (default = [1,column]) (VPA). */ + let row = unsafe { std::str::from_utf8_unchecked(buf) } + .parse::() + .unwrap(); + debug!( + "Line position absolute row {} with cursor at {:?}", + row, cursor + ); + cursor.1 = row.saturating_sub(1); + if scroll_region.top + cursor.1 >= terminal_size.1 { + cursor.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(); + *state = State::Csi2(buf1, buf2); + } + (b'm', State::Csi1(ref buf1)) => { + // Character Attributes. + match buf1.as_slice() { + b"30" => *fg_color = Color::Black, + b"31" => *fg_color = Color::Red, + b"32" => *fg_color = Color::Green, + b"33" => *fg_color = Color::Yellow, + b"34" => *fg_color = Color::Blue, + b"35" => *fg_color = Color::Magenta, + b"36" => *fg_color = Color::Cyan, + b"37" => *fg_color = Color::White, + + b"39" => *fg_color = Color::Default, + b"40" => *fg_color = Color::Black, + b"41" => *bg_color = Color::Red, + b"42" => *bg_color = Color::Green, + b"43" => *bg_color = Color::Yellow, + b"44" => *bg_color = Color::Blue, + b"45" => *bg_color = Color::Magenta, + b"46" => *bg_color = Color::Cyan, + b"47" => *bg_color = Color::White, + + b"49" => *bg_color = Color::Default, + _ => {} + } + grid[cursor_val!()].set_fg(*fg_color); + grid[cursor_val!()].set_bg(*bg_color); + *state = State::Normal; + } + (c, State::Csi1(ref mut buf)) if (c >= b'0' && c <= b'9') || c == b' ' => { + buf.push(c); + } + (_, State::Csi1(_)) => { + debug!("ignoring unknown code {}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + (b';', State::Csi2(ref mut buf1_p, ref mut buf2_p)) => { + let buf1 = std::mem::replace(buf1_p, Vec::new()); + let buf2 = std::mem::replace(buf2_p, Vec::new()); + let buf3 = Vec::new(); + *state = State::Csi3(buf1, buf2, buf3); + } + (b't', State::Csi2(_, _)) => { + debug!("ignoring {}", EscCode::from((&(*state), byte))); + // Window manipulation, skip it + *state = State::Normal; + } + (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_or(1); + let orig_y = unsafe { std::str::from_utf8_unchecked(y) } + .parse::() + .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 { + 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 + ); + } + if scroll_region.top + cursor.1 >= terminal_size.1 { + cursor.1 = terminal_size.1.saturating_sub(1); + } + debug!("cursor became: {:?}", cursor); + *state = State::Normal; + } + (c, State::Csi2(_, ref mut buf)) if c >= b'0' && c <= b'9' => { + buf.push(c); + } + (b'r', State::Csi2(ref top, ref bottom)) => { + /* CSI Ps ; Ps r Set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM). */ + let top = unsafe { std::str::from_utf8_unchecked(top) } + .parse::() + .unwrap_or(1); + let bottom = unsafe { std::str::from_utf8_unchecked(bottom) } + .parse::() + .unwrap_or(1); + + if bottom > top { + scroll_region.top = top - 1; + scroll_region.bottom = bottom - 1; + *cursor = (0, 0); + } + debug!("set scrolling region to {:?}", scroll_region); + *state = State::Normal; + } + (_, State::Csi2(_, _)) => { + debug!("ignoring unknown code {}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + (b't', State::Csi3(_, _, _)) => { + debug!("ignoring {}", EscCode::from((&(*state), byte))); + // Window manipulation, skip it + *state = State::Normal; + } + + (c, State::Csi3(_, _, ref mut buf)) if c >= b'0' && c <= b'9' => { + buf.push(c); + } + (b'm', State::Csi3(ref buf1, ref buf2, ref buf3)) if buf1 == b"38" && buf2 == b"5" => { + /* Set character attributes | foreground color */ + *fg_color = if let Ok(byte) = + u8::from_str_radix(unsafe { std::str::from_utf8_unchecked(buf3) }, 10) + { + debug!("parsed buf as {}", byte); + Color::Byte(byte) + } else { + Color::Default + }; + grid[cursor_val!()].set_fg(*fg_color); + debug!("{}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + (b'm', State::Csi3(ref buf1, ref buf2, ref buf3)) if buf1 == b"48" && buf2 == b"5" => { + /* Set character attributes | background color */ + *bg_color = if let Ok(byte) = + u8::from_str_radix(unsafe { std::str::from_utf8_unchecked(buf3) }, 10) + { + debug!("parsed buf as {}", byte); + Color::Byte(byte) + } else { + Color::Default + }; + grid[cursor_val!()].set_bg(*bg_color); + debug!("{}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + (_, State::Csi3(_, _, _)) => { + debug!("ignoring unknown code {}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + /* other stuff */ + (_, State::G0) => { + debug!("ignoring {}", EscCode::from((&(*state), byte))); + *state = State::Normal; + } + } + } +} diff --git a/ui/src/terminal/keys.rs b/ui/src/terminal/keys.rs index ebf4cfe2..84604712 100644 --- a/ui/src/terminal/keys.rs +++ b/ui/src/terminal/keys.rs @@ -22,7 +22,6 @@ use super::*; use crossbeam::{channel::Receiver, select}; use serde::{Serialize, Serializer}; -use std::io; use termion::event::Event as TermionEvent; use termion::event::Key as TermionKey; use termion::input::TermRead; @@ -139,8 +138,18 @@ impl PartialEq for &Key { enum InputMode { Normal, Paste, + PasteRaw(Vec), } +#[derive(Debug)] +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,24 @@ 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 => return, + InputCommand::Raw => { + get_events_raw(closure, closure_raw, rx); + return; + } + InputCommand::NoRaw => unreachable!(), } } }; @@ -190,6 +201,60 @@ pub fn get_events( } } } + +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 => return, + InputCommand::NoRaw => { + get_events(closure_nonraw, closure, rx); + return; + } + 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. + } + } +} const FIELDS: &[&str] = &[]; impl<'de> Deserialize<'de> for Key { diff --git a/ui/src/types.rs b/ui/src/types.rs index 1ad54160..1c61f28c 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), @@ -64,7 +66,10 @@ impl From for ThreadEvent { #[derive(Debug)] pub enum ForkType { - Finished, // Already finished fork, we only want to restore input/output + /// Already finished fork, we only want to restore input/output + Finished, + /// Embed pty + Embed, Generic(std::process::Child), NewDraft(File, std::process::Child), } @@ -81,6 +86,7 @@ pub enum UIEvent { Input(Key), ExInput(Key), InsertInput(Key), + EmbedInput((Key, Vec)), RefreshMailbox((usize, FolderHash)), //view has changed to FolderHash mailbox //Quit? Resize, @@ -111,6 +117,8 @@ impl From for UIEvent { pub enum UIMode { Normal, Insert, + /// Forward input to an embed pseudoterminal. + Embed, Execute, Fork, } @@ -125,6 +133,7 @@ impl fmt::Display for UIMode { UIMode::Insert => "INSERT", UIMode::Execute => "EX", UIMode::Fork => "FORK", + UIMode::Embed => "EMBED", } ) }