use libc::{termios, winsize}; use std::error::Error; use std::io::{stdout, BufRead, Write}; const CTRL_Q: u8 = 17; const CTRL_S: u8 = 19; const CTRL_Y: u8 = 25; const CTRL_Z: u8 = 26; const BACKSPACE: u8 = 127; const OTHER_BACKSPACE: u8 = 8; const ESC: u8 = 27; pub fn size() -> Result> { unsafe { let mut window: winsize = std::mem::zeroed(); let status = libc::ioctl(1, libc::TIOCGWINSZ, &mut window); if status < 0 { return Err(Box::from(format!( "terminal size: ioctl failed with code {}", status ))); } return Ok(window); } } fn get_attr() -> Result> { unsafe { let mut attr: termios = std::mem::zeroed(); let status = libc::tcgetattr(1, &mut attr); if status < 0 { return Err(Box::from(format!("tcgetattr failed with code {}", status))); } return Ok(attr); } } fn set_attr(attrs: &termios) -> Result<(), Box> { unsafe { let status = libc::tcsetattr(1, libc::TCSANOW, attrs); if status < 0 { return Err(Box::from(format!("set attributes: code {}", status))); } return Ok(()); } } fn make_raw() -> termios { unsafe { let mut attr: termios = std::mem::zeroed(); libc::cfmakeraw(&mut attr); attr.c_cc[libc::VMIN] = 0; // Return zero bytes on timeout. attr.c_cc[libc::VTIME] = 1; // 100 ms timeout. return attr; } } pub struct RawHandle { old: termios, } impl Drop for RawHandle { fn drop(&mut self) { let _ = write!(stdout(), "\x1b[?7h\x1b[H\x1b[J"); let _ = stdout().flush(); let _ = set_attr(&self.old); } } pub fn raw() -> Result> { write!(stdout(), "\x1b[?7l")?; stdout().flush()?; let old = get_attr()?; set_attr(&make_raw())?; return Ok(RawHandle { old: old }); } #[derive(Debug)] pub enum Key { CtrlQ, CtrlS, CtrlZ, CtrlY, Up, Down, Left, Right, Backspace, Char(u8), Timeout, } pub fn read_key(stdin: &mut dyn BufRead) -> Result> { loop { let mut buf = vec![0; 1]; let n = stdin.read(&mut buf)?; if n == 0 { return Ok(Key::Timeout); } if buf[0] == CTRL_Q { return Ok(Key::CtrlQ); } if buf[0] == CTRL_S { return Ok(Key::CtrlS); } if buf[0] == CTRL_Y { return Ok(Key::CtrlY); } if buf[0] == CTRL_Z { return Ok(Key::CtrlZ); } if buf[0] == BACKSPACE || buf[0] == OTHER_BACKSPACE { return Ok(Key::Backspace); } if buf[0] != ESC { return Ok(Key::Char(buf[0])); } // Try to handle an escape sequence. let buf = stdin.fill_buf()?; if buf.len() == 0 { // Just an escape. Ignore it I guess. continue; } if buf[0] != b'[' { // Unknown escape sequence, just read another key. stdin.consume(1); continue; } let mut n = buf.len(); for (i, &c) in buf.iter().enumerate() { match c { // Escape sequences usually end with a letter. b'A'..=b'Z' | b'a'..=b'z' => { n = i + 1; break; } _ => (), } } let mut escape_sequence = Vec::new(); escape_sequence.extend_from_slice(&buf[1..n]); stdin.consume(n); if escape_sequence.len() < 1 { continue; } match escape_sequence[0] { b'A' => { return Ok(Key::Up); } b'B' => { return Ok(Key::Down); } b'C' => { return Ok(Key::Right); } b'D' => { return Ok(Key::Left); } _ => { // Unknown escape, just skip it. continue; } } } } fn read_status_report(stdin: &mut dyn BufRead) -> Result<(u16, u16), Box> { let buf = stdin.fill_buf()?; let mut semicolon = buf.len(); let mut r = buf.len(); for (i, &c) in buf.iter().enumerate() { if c == b';' { semicolon = i; } if c == b'R' { r = i; break; } } if buf.len() < 6 || buf[0] != ESC || buf[1] != b'[' || semicolon + 1 > r || r == buf.len() { return Err(Box::from(format!( "invalid response: {:?}", String::from_utf8_lossy(&buf[0..r]) ))); } let row_str = std::str::from_utf8(&buf[2..semicolon])?; let col_str = std::str::from_utf8(&buf[semicolon + 1..r])?; let row = row_str.parse::()?; let col = col_str.parse::()?; stdin.consume(r + 1); // Yuck... 1 indexing return Ok((row - 1, col - 1)); } pub fn cursor_pos(stdin: &mut dyn BufRead) -> Result<(u16, u16), Box> { write!(stdout(), "\x1b[6n")?; stdout().flush()?; let pos = read_status_report(stdin)?; return Ok(pos); }