use crate::error::{Error, Result}; use std::fmt; use std::io::{self, Read}; use std::ops::{Deref, DerefMut}; use std::os::unix::io::AsRawFd; use std::str; pub struct StdinRawMode { stdin: io::Stdin, orig: termios::Termios, } // TODO: Separate editor into frontend and backend. In frontend, it handles actual screen and user input. // It interacts with backend by responding to request from frontend. Frontend focues on core editor // logic. This is useful when adding a new frontend (e.g. wasm). impl StdinRawMode { pub fn new() -> Result { use termios::*; let stdin = io::stdin(); let fd = stdin.as_raw_fd(); let mut termios = Termios::from_fd(fd)?; let orig = termios; // Set terminal raw mode. Disable echo back, canonical mode, signals (SIGINT, SIGTSTP) and Ctrl+V. termios.c_lflag &= !(ECHO | ICANON | ISIG | IEXTEN); // Disable control flow mode (Ctrl+Q/Ctrl+S) and CR-to-NL translation termios.c_iflag &= !(IXON | ICRNL | BRKINT | INPCK | ISTRIP); // Disable output processing such as \n to \r\n translation termios.c_oflag &= !OPOST; // Ensure character size is 8bits termios.c_cflag |= CS8; // Implement blocking read for efficient reading of input termios.c_cc[VMIN] = 1; termios.c_cc[VTIME] = 0; // Apply terminal configurations tcsetattr(fd, TCSAFLUSH, &termios)?; Ok(StdinRawMode { stdin, orig }) } pub fn input_keys(self) -> InputSequences { InputSequences { stdin: self } } } impl Drop for StdinRawMode { fn drop(&mut self) { // Restore original terminal mode termios::tcsetattr(self.stdin.as_raw_fd(), termios::TCSAFLUSH, &self.orig).unwrap(); } } impl Deref for StdinRawMode { type Target = io::Stdin; fn deref(&self) -> &Self::Target { &self.stdin } } impl DerefMut for StdinRawMode { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.stdin } } #[derive(PartialEq, Debug, Clone)] pub enum KeySeq { Unidentified, Utf8Key(char), Key(u8), // Char code and ctrl mod LeftKey, RightKey, UpKey, DownKey, PageUpKey, PageDownKey, HomeKey, EndKey, DeleteKey, Cursor(usize, usize), // Pseudo key (x, y) } impl fmt::Display for KeySeq { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use KeySeq::*; match self { Unidentified => write!(f, "UNKNOWN"), Key(b' ') => write!(f, "SPACE"), Key(b) if b.is_ascii_control() => write!(f, "\\x{:x}", b), Key(b) => write!(f, "{}", *b as char), Utf8Key(c) => write!(f, "{}", c), LeftKey => write!(f, "LEFT"), RightKey => write!(f, "RIGHT"), UpKey => write!(f, "UP"), DownKey => write!(f, "DOWN"), PageUpKey => write!(f, "PAGEUP"), PageDownKey => write!(f, "PAGEDOWN"), HomeKey => write!(f, "HOME"), EndKey => write!(f, "END"), DeleteKey => write!(f, "DELETE"), Cursor(r, c) => write!(f, "CURSOR({},{})", r, c), } } } #[derive(PartialEq, Debug, Clone)] pub struct InputSeq { pub key: KeySeq, pub ctrl: bool, pub alt: bool, } impl InputSeq { pub fn new(key: KeySeq) -> Self { Self { key, ctrl: false, alt: false, } } pub fn ctrl(key: KeySeq) -> Self { Self { key, ctrl: true, alt: false, } } pub fn alt(key: KeySeq) -> Self { Self { key, ctrl: false, alt: true, } } } impl fmt::Display for InputSeq { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.ctrl { write!(f, "C-")?; } if self.alt { write!(f, "M-")?; } write!(f, "{}", self.key) } } pub struct InputSequences { stdin: StdinRawMode, } impl InputSequences { fn read_byte(&mut self) -> Result> { let mut one_byte: [u8; 1] = [0]; Ok(if self.stdin.read(&mut one_byte)? == 0 { None } else { Some(one_byte[0]) }) } fn decode_escape_sequence(&mut self) -> Result { use KeySeq::*; // Try to read expecting '[' as escape sequence header. Note that, if next input does // not arrive within next tick, it means that it is not an escape sequence. // TODO?: Should we consider sequences not starting with '['? match self.read_byte()? { Some(b'[') => { /* fall through */ } Some(b) if b.is_ascii_control() => return Ok(InputSeq::new(Key(0x1b))), // Ignore control characters after ESC Some(b) => { // Alt key is sent as ESC prefix (e.g. Alt-A => \x1b\x61 // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Alt-and-Meta-Keys let mut seq = self.decode(b)?; seq.alt = true; return Ok(seq); } None => return Ok(InputSeq::new(Key(0x1b))), }; // Now confirmed \1xb[ which is a header of escape sequence. Eat it until the end // of sequence with blocking let mut buf = vec![]; let cmd = loop { if let Some(b) = self.read_byte()? { match b { // Control command chars from http://ascii-table.com/ansi-escape-sequences-vt-100.php b'A' | b'B' | b'C' | b'D' | b'F' | b'H' | b'K' | b'J' | b'R' | b'c' | b'f' | b'g' | b'h' | b'l' | b'm' | b'n' | b'q' | b't' | b'y' | b'~' => break b, _ => buf.push(b), } } else { // Unknown escape sequence ignored return Ok(InputSeq::new(Unidentified)); } }; fn parse_bytes_as_usize(b: &[u8]) -> Option { str::from_utf8(b).ok().and_then(|s| s.parse().ok()) } let mut args = buf.split(|b| *b == b';'); match cmd { b'R' => { // https://vt100.net/docs/vt100-ug/chapter3.html#CPR e.g. \x1b[24;80R let mut i = args.filter_map(parse_bytes_as_usize); match (i.next(), i.next()) { (Some(r), Some(c)) => Ok(InputSeq::new(Cursor(r, c))), _ => Ok(InputSeq::new(Unidentified)), } } // e.g. => \x1b[C // e.g. C- => \x1b[1;5C b'A' | b'B' | b'C' | b'D' => { let key = match cmd { b'A' => UpKey, b'B' => DownKey, b'C' => RightKey, b'D' => LeftKey, _ => unreachable!(), }; let ctrl = args.next() == Some(b"1") && args.next() == Some(b"5"); let alt = false; Ok(InputSeq { key, ctrl, alt }) } b'~' => { // e.g. \x1b[5~ match args.next() { Some(b"5") => Ok(InputSeq::new(PageUpKey)), Some(b"6") => Ok(InputSeq::new(PageDownKey)), Some(b"1") | Some(b"7") => Ok(InputSeq::new(HomeKey)), Some(b"4") | Some(b"8") => Ok(InputSeq::new(EndKey)), Some(b"3") => Ok(InputSeq::new(DeleteKey)), _ => Ok(InputSeq::new(Unidentified)), } } b'H' | b'F' => { // C-HOME => \x1b[1;5H let key = match cmd { b'H' => HomeKey, b'F' => EndKey, _ => unreachable!(), }; let ctrl = args.next() == Some(b"1") && args.next() == Some(b"5"); let alt = false; Ok(InputSeq { key, ctrl, alt }) } _ => unreachable!(), } } fn decode_utf8(&mut self, b: u8) -> Result { // TODO: Use arrayvec crate let mut buf = [0; 4]; buf[0] = b; let mut len = 1; loop { if let Some(b) = self.read_byte()? { buf[len] = b; len += 1; } else { return Err(Error::NotUtf8Input(buf[..len].to_vec())); } if let Ok(s) = str::from_utf8(&buf) { return Ok(InputSeq::new(KeySeq::Utf8Key(s.chars().next().unwrap()))); } if len == 4 { return Err(Error::NotUtf8Input(buf.to_vec())); } } } fn decode(&mut self, b: u8) -> Result { use KeySeq::*; match b { // C0 control characters 0x00..=0x1f => match b { // (Maybe) Escape sequence. Ctrl-{ is not available due to this 0x1b => self.decode_escape_sequence(), // Ctrl-SPACE and Ctrl-?. 0x40, 0x3f, 0x60, 0x5f are not available 0x00 | 0x1f => Ok(InputSeq::ctrl(Key(b | 0b0010_0000))), // Ctrl-\ and Ctrl-]. 0x3c, 0x3d, 0x7c, 0x7d are not available 0x01c | 0x01d => Ok(InputSeq::ctrl(Key(b | 0b0100_0000))), // 0x00~0x1f keys are ascii keys with ctrl. Ctrl mod masks key with 0b11111. // Here unmask it with 0b1100000. It only works with 0x61~0x7f. _ => Ok(InputSeq::ctrl(Key(b | 0b0110_0000))), }, // Printable ASCII characters (0x20..0x7f) // Ascii key inputs 0x20..=0x7f => Ok(InputSeq::new(Key(b))), // C1 control characters https://en.wikipedia.org/wiki/C0_and_C1_control_codes#C1_controls // XXX: Kiro ignores them since I have no idea how to handle them on terminal. Perhaps // SSA and ESA can be handled as selecting region. 0x80..=0x9f => Ok(InputSeq::new(KeySeq::Unidentified)), // Other UTF-8 characters 0xa0..=0xff => self.decode_utf8(b), } } fn read_seq(&mut self) -> Result { if let Some(b) = self.read_byte()? { self.decode(b) } else { Ok(InputSeq::new(KeySeq::Unidentified)) } } } impl Iterator for InputSequences { type Item = Result; // Read next byte from stdin, if nothing was read, it returns InputSeq::Unidentified. // This method never returns None so for loop never ends. fn next(&mut self) -> Option { Some(self.read_seq()) } }