diff --git a/src/bin.rs b/src/bin.rs index 9e97cc71..158e79b9 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -305,7 +305,8 @@ fn main() -> std::result::Result<(), std::io::Error> { }, } }, - UIMode::Embed => {}, + UIMode::Embed => state.redraw(), + UIMode::Fork => { break 'inner; // `goto` 'reap loop, and wait on child. }, diff --git a/ui/src/components/mail/compose.rs b/ui/src/components/mail/compose.rs index 0319a3f3..cb81f9e1 100644 --- a/ui/src/components/mail/compose.rs +++ b/ui/src/components/mail/compose.rs @@ -546,38 +546,34 @@ impl Component for Composer { /* Regardless of view mode, do the following */ self.form.draw(grid, header_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; - } + 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))), + ); + 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 { @@ -766,6 +762,15 @@ impl Component for Composer { match *event { UIEvent::Resize => { self.set_dirty(); + if let Some(ref mut embed_pty) = self.embed { + match embed_pty { + EmbedStatus::Running(_, _) => { + let mut guard = embed_pty.lock().unwrap(); + guard.grid.clear(Cell::default()); + } + _ => {} + } + } } /* /* Switch e-mail From: field to the `left` configured account. */ @@ -825,7 +830,7 @@ impl Component for Composer { } return true; } - UIEvent::ChildStatusExited(ref pid, ref exit_code) + UIEvent::ChildStatusExited(ref pid, _) if self.embed.is_some() && *pid == self @@ -835,6 +840,7 @@ impl Component for Composer { .unwrap() => { self.embed = None; + self.set_dirty(); self.mode = ViewMode::Edit; context .replies @@ -855,6 +861,7 @@ impl Component for Composer { } _ => {} } + self.set_dirty(); context .replies .push_back(UIEvent::ChangeMode(UIMode::Normal)); @@ -874,6 +881,7 @@ impl Component for Composer { } _ => {} } + self.set_dirty(); context .replies .push_back(UIEvent::ChangeMode(UIMode::Embed)); @@ -884,6 +892,7 @@ impl Component for Composer { context .replies .push_back(UIEvent::ChangeMode(UIMode::Normal)); + self.dirty = true; } UIEvent::EmbedInput((ref k, ref b)) => { use std::io::Write; @@ -965,7 +974,7 @@ impl Component for Composer { } } } - self.dirty = true; + self.set_dirty(); return true; } UIEvent::Input(Key::Char('e')) if self.mode.is_embed() => { @@ -973,6 +982,7 @@ impl Component for Composer { context .replies .push_back(UIEvent::ChangeMode(UIMode::Embed)); + self.set_dirty(); return true; } UIEvent::Input(Key::Char('e')) => { @@ -1114,14 +1124,19 @@ impl Component for Composer { } fn is_dirty(&self) -> bool { - self.dirty - || self.pager.is_dirty() - || self - .reply_context - .as_ref() - .map(|(_, p)| p.is_dirty()) - .unwrap_or(false) - || self.form.is_dirty() + match self.mode { + ViewMode::Embed => true, + _ => { + self.dirty + || self.pager.is_dirty() + || self + .reply_context + .as_ref() + .map(|(_, p)| p.is_dirty()) + .unwrap_or(false) + || self.form.is_dirty() + } + } } fn set_dirty(&mut self) { diff --git a/ui/src/terminal/embed.rs b/ui/src/terminal/embed.rs index 78f38c81..de19ffe3 100644 --- a/ui/src/terminal/embed.rs +++ b/ui/src/terminal/embed.rs @@ -8,34 +8,26 @@ 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::{self, waitpid}, -}; -use nix::unistd::{dup2, fork, setsid, ForkResult, Pid}; +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; -use crate::terminal::cells::{Cell, CellBuffer}; pub use grid::EmbedGrid; -use grid::*; // ioctl command to set window size of pty: use libc::TIOCSWINSZ; use std::path::Path; -use std::process::{Command, Stdio}; 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); -static SWITCHALTERNATIVE_1049: &'static [u8] = &[b'1', b'0', b'4', b'9']; - pub fn create_pty(area: Area, command: String) -> nix::Result>> { // Open a new PTY master let master_fd = posix_openpt(OFlag::O_RDWR)?; @@ -68,7 +60,7 @@ pub fn create_pty(area: Area, command: String) -> nix::Result { - // assign stdin, stdout, stderr to the tty, just like a terminal does + // 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(); @@ -136,6 +128,7 @@ pub enum State { Normal, } +/* Used for debugging */ struct EscCode<'a>(&'a State, u8); impl<'a> From<(&'a mut State, u8)> for EscCode<'a> { @@ -317,7 +310,9 @@ impl std::fmt::Display for EscCode<'_> { EscCode(CsiQ(ref buf), c) => { write!(f, "ESC[?{}{}\t\tCSI [UNKNOWN]", unsafestr!(buf), *c as char) } - _ => unreachable!(), + EscCode(unknown, c) => { + write!(f, "{:?}{} [UNKNOWN]", unknown, c) + } } } } diff --git a/ui/src/terminal/embed/grid.rs b/ui/src/terminal/embed/grid.rs index 8e3ae8a2..37b17ce3 100644 --- a/ui/src/terminal/embed/grid.rs +++ b/ui/src/terminal/embed/grid.rs @@ -3,38 +3,98 @@ 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}; +/** + * `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), - resized: bool, + 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, - resized: false, + 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); + use std::convert::TryFrom; + 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(); - self.resized = true; } pub fn wake_up(&self) { @@ -54,27 +114,50 @@ impl EmbedGrid { 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 resized, + 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 == *terminal_size { - /* do nothing */ - } else if cursor.0 + 1 >= terminal_size.0 { - //cursor.0 = 0; - //cursor.1 += 1; - } else { + 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) => { @@ -91,7 +174,7 @@ impl EmbedGrid { *state = State::G0; } (b'J', State::ExpectingControlChar) => { - // "ESCJ Erase from the cursor to the end of the screen" + // 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 { @@ -102,14 +185,14 @@ impl EmbedGrid { *state = State::Normal; } (b'K', State::ExpectingControlChar) => { - // "ESCK Erase from the cursor to the end of the line" + // 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) => { + (_, State::ExpectingControlChar) => { debug!( "unrecognised: byte is {} and state is {:?}", byte as char, state @@ -120,9 +203,6 @@ impl EmbedGrid { 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); @@ -135,21 +215,16 @@ impl EmbedGrid { (c, State::Osc2(_, ref mut buf)) if (c >= b'0' && c <= b'9') || c == b'?' => { buf.push(c); } - (c, State::Osc1(_)) => { - debug!("ignoring {}", EscCode::from((&(*state), byte))); + (_, State::Osc1(_)) => { + debug!("ignoring unknown code {}", EscCode::from((&(*state), byte))); *state = State::Normal; } - (c, State::Osc2(_, _)) => { - debug!("ignoring {}", EscCode::from((&(*state), byte))); + (_, State::Osc2(_, _)) => { + debug!("ignoring unknown code {}", EscCode::from((&(*state), byte))); *state = State::Normal; } - /* END OF OSC */ - /* ********** */ - /* ********** */ - /* ********** */ - /* ********** */ + /* Normal */ (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); @@ -165,47 +240,112 @@ impl EmbedGrid { (b'', State::Normal) => { debug!("Visual bell ^G, ignoring {:?}", cursor); } - /* Backspace */ (0x08, State::Normal) => { - //debug!("setting cell {:?} char '{}'", cursor, c as char); + /* Backspace */ 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[*cursor].set_ch(c as char); - debug!("setting cell {:?} char '{}'", cursor, c as char); + /* 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!("ignoring {}", EscCode::from((&(*state), byte))); + debug!("restore cursor {}", EscCode::from((&(*state), byte))); + *show_cursor = true; *state = State::Normal; } (b'm', State::Csi) => { - /* Character Attributes (SGR). Ps = 0 -> Normal (default), VT100 */ - debug!("ignoring {}", EscCode::from((&(*state), byte))); + /* 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!("sending {}", EscCode::from((&(*state), byte)),); + /* 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!("sending {}", EscCode::from((&(*state), byte)),); - grid[*cursor].set_ch(' '); + /* delete one character */ + debug!("{}", EscCode::from((&(*state), byte)),); + grid[cursor_val!()].set_ch(' '); *state = State::Normal; } (b'C', State::Csi) => { - // "ESC[C\t\tCSI Cursor Forward one Time", + // 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); @@ -215,17 +355,46 @@ impl EmbedGrid { (c, State::CsiQ(ref mut buf)) if c >= b'0' && c <= b'9' => { buf.push(c); } - (c, State::CsiQ(ref mut buf)) => { - // we are already in AlternativeScreen so do not forward this - if &buf.as_slice() != &SWITCHALTERNATIVE_1049 { - debug!("ignoring {}", EscCode::from((&(*state), byte))); + (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); @@ -239,7 +408,10 @@ impl EmbedGrid { ( ( 0, - std::cmp::min(cursor.1 + 1, terminal_size.1.saturating_sub(1)), + std::cmp::min( + cursor.1 + 1 + scroll_region.top, + terminal_size.1.saturating_sub(1), + ), ), ( terminal_size.0.saturating_sub(1), @@ -255,12 +427,38 @@ impl EmbedGrid { /* Erase to right (Default) */ debug!("{}", EscCode::from((&(*state), byte))); for x in cursor.0..terminal_size.0 { - grid[(x, cursor.1)] = Cell::default(); + grid[(x, cursor.1 + scroll_region.top)] = Cell::default(); } *state = State::Normal; } - (c, State::Csi) => { - debug!("ignoring {}", EscCode::from((&(*state), byte))); + (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" => { @@ -268,7 +466,7 @@ impl EmbedGrid { /* Erase to right (Default) */ debug!("{}", EscCode::from((&(*state), byte))); for x in cursor.0..terminal_size.0 { - grid[(x, cursor.1)] = Cell::default(); + grid[(x, cursor.1 + scroll_region.top)] = Cell::default(); } *state = State::Normal; } @@ -276,7 +474,7 @@ impl EmbedGrid { /* Erase in Line (ED), VT100.*/ /* Erase to left (Default) */ for x in cursor.0..=0 { - grid[(x, cursor.1)] = Cell::default(); + grid[(x, cursor.1 + scroll_region.top)] = Cell::default(); } debug!("{}", EscCode::from((&(*state), byte))); *state = State::Normal; @@ -301,7 +499,10 @@ impl EmbedGrid { ( ( 0, - std::cmp::min(cursor.1 + 1, terminal_size.1.saturating_sub(1)), + std::cmp::min( + cursor.1 + 1 + scroll_region.top, + terminal_size.1.saturating_sub(1), + ), ), ( terminal_size.0.saturating_sub(1), @@ -321,7 +522,7 @@ impl EmbedGrid { (0, 0), ( terminal_size.0.saturating_sub(1), - cursor.1.saturating_sub(1), + cursor.1.saturating_sub(1) + scroll_region.top, ), ), ); @@ -338,15 +539,38 @@ impl EmbedGrid { (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))); + 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))); - // P s = 1 8 → Report the size of the text area in characters as CSI 8 ; height ; width t stdin.write_all(b"\x1b[8;").unwrap(); stdin .write_all((terminal_size.1).to_string().as_bytes()) @@ -357,17 +581,16 @@ impl EmbedGrid { .unwrap(); stdin.write_all(&[b't']).unwrap(); } else { - debug!("not sending {}", EscCode::from((&(*state), byte))); + debug!("ignoring unknown code {}", EscCode::from((&(*state), byte))); } *state = State::Normal; } (b'n', State::Csi1(_)) => { - /* report cursor position */ + // 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(); - // Ps = 6 ⇒ Report Cursor Position (CPR) [row;column]. - //Result is CSI r ; c R stdin .write_all((cursor.1 + 1).to_string().as_bytes()) .unwrap(); @@ -378,8 +601,31 @@ impl EmbedGrid { 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\t\tCSI Cursor Down {buf} Times", + // ESC[{buf}B CSI Cursor Down {buf} Times let offset = unsafe { std::str::from_utf8_unchecked(buf) } .parse::() .unwrap(); @@ -387,23 +633,14 @@ impl EmbedGrid { if offset + cursor.1 < terminal_size.1 { cursor.1 += offset; } - debug!("cursor became: {:?}", cursor); - *state = State::Normal; - } - (b'C', State::Csi1(buf)) => { - // "ESC[{buf}C\t\tCSI Cursor Forward {buf} Times", - let offset = unsafe { std::str::from_utf8_unchecked(buf) } - .parse::() - .unwrap(); - debug!("cursor forward {} times, cursor was: {:?}", offset, cursor); - if offset + cursor.0 < terminal_size.0 { - cursor.0 += 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\t\tCSI Cursor Backward {buf} Times", + // ESC[{buf}D CSI Cursor Backward {buf} Times let offset = unsafe { std::str::from_utf8_unchecked(buf) } .parse::() .unwrap(); @@ -415,7 +652,7 @@ impl EmbedGrid { *state = State::Normal; } (b'E', State::Csi1(buf)) => { - //"ESC[{buf}E\t\tCSI Cursor Next Line {buf} Times", + // ESC[{buf}E CSI Cursor Next Line {buf} Times let offset = unsafe { std::str::from_utf8_unchecked(buf) } .parse::() .unwrap(); @@ -425,18 +662,20 @@ impl EmbedGrid { ); if offset + cursor.1 < terminal_size.1 { cursor.1 += offset; - //cursor.0 = 0; + } + 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\t\tCursor Character Absolute [column={buf}] (default = [row,1])", + // 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 + 1 { + if new_col < terminal_size.0 { cursor.0 = new_col.saturating_sub(1); } else { debug!( @@ -448,7 +687,7 @@ impl EmbedGrid { *state = State::Normal; } (b'C', State::Csi1(buf)) => { - // "ESC[{buf}C\t\tCSI Cursor Preceding Line {buf} Times", + // ESC[{buf}C CSI Cursor Preceding Line {buf} Times let offset = unsafe { std::str::from_utf8_unchecked(buf) } .parse::() .unwrap(); @@ -458,13 +697,15 @@ impl EmbedGrid { ); if cursor.1 >= offset { cursor.1 -= offset; - //cursor.0 = 0; + } + 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\t\tCSI Delete {buf} characters, default = 1", + // ESC[{buf}P CSI Delete {buf} characters, default = 1 let offset = unsafe { std::str::from_utf8_unchecked(buf) } .parse::() .unwrap(); @@ -473,13 +714,13 @@ impl EmbedGrid { offset, cursor ); for x in (cursor.0 - std::cmp::min(offset, cursor.0))..cursor.0 { - grid[(x, cursor.1)].set_ch(' '); + grid[(x, cursor.1 + scroll_region.top)].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)) => { + /* CSI Pm d Line Position Absolute [row] (default = [1,column]) (VPA). */ let row = unsafe { std::str::from_utf8_unchecked(buf) } .parse::() .unwrap(); @@ -487,7 +728,10 @@ impl EmbedGrid { "Line position absolute row {} with cursor at {:?}", row, cursor ); - cursor.1 = std::cmp::min(row.saturating_sub(1), terminal_size.1.saturating_sub(1)); + 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; } @@ -496,11 +740,40 @@ impl EmbedGrid { 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); } - (c, State::Csi1(ref buf)) => { - debug!("ignoring {}", EscCode::from((&(*state), byte))); + (_, State::Csi1(_)) => { + debug!("ignoring unknown code {}", EscCode::from((&(*state), byte))); *state = State::Normal; } (b';', State::Csi2(ref mut buf1_p, ref mut buf2_p)) => { @@ -509,11 +782,6 @@ impl EmbedGrid { let buf3 = Vec::new(); *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 @@ -532,7 +800,7 @@ impl EmbedGrid { "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 { @@ -541,14 +809,34 @@ impl EmbedGrid { 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); } - (c, State::Csi2(ref buf1, ref buf2)) => { - debug!("ignoring {}", EscCode::from((&(*state), byte))); + (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(_, _, _)) => { @@ -560,21 +848,43 @@ impl EmbedGrid { (c, State::Csi3(_, _, ref mut buf)) if c >= b'0' && c <= b'9' => { buf.push(c); } - (c, State::Csi3(_, _, _)) => { - debug!("ignoring {}", EscCode::from((&(*state), byte))); + (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 */ - /* ******************* */ - /* ******************* */ - /* ******************* */ - (c, State::G0) => { + (_, State::G0) => { debug!("ignoring {}", EscCode::from((&(*state), byte))); *state = State::Normal; } - (b, s) => { - debug!("unrecognised: byte is {} and state is {:?}", b as char, s); - } } } } diff --git a/ui/src/terminal/keys.rs b/ui/src/terminal/keys.rs index a349d9fe..945ae9a3 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;