use std::{collections::BTreeMap, io::Read, os::unix::ffi::OsStrExt, sync::RwLock}; use crate::cursor::Direction; pub enum KeyboardInput { Eof, Key(u8), CtrlA, CtrlB, CtrlC, CtrlE, CtrlD, CtrlL, CtrlR, Arrow(Direction), CtrlArrow(Direction), DeleteLeft, DeleteRight, CtrlDeleteRight, Home, End, } fn read1() -> Option { let mut buf = [0]; match std::io::stdin().lock().read_exact(&mut buf) { Ok(_) => Some(buf[0]), Err(_) => None, } } fn byte_to_dir(b: u8) -> Option { use Direction::*; match b { b'A' => Some(Up), b'B' => Some(Down), b'C' => Some(Right), b'D' => Some(Left), _ => None, } } fn read_escape(debug: bool) -> KeyboardInput { use Direction::*; use KeyboardInput::*; let mut seq = vec![match read1() { Some(x) => x, None => return Eof, }]; if seq[0] == b'[' { // still more while { let last = seq[seq.len() - 1]; !(0x40..=0x7E).contains(&last) || seq.len() == 1 } { seq.push(match read1() { Some(x) => x, None => return Eof, }); } if debug { println!("escape: {}", seq.escape_ascii()); } match seq[1] { b'3' => { if seq.len() > 2 && seq[2] == b'~' { DeleteRight } else { todo!("unhandled: {}", seq.escape_ascii()); } } b'H' => Home, b'F' => End, b'd' => CtrlDeleteRight, // Ctrl Arrow b'1' => { if seq[1..].starts_with(b"1;5") { if seq.len() == 4 { todo!("idk what this is."); } match seq[4] { b'A' => CtrlArrow(Up), b'B' => CtrlArrow(Down), b'C' => CtrlArrow(Right), b'D' => CtrlArrow(Left), _ => todo!("unhandled {}", seq.escape_ascii()), } } else { todo!("unhandled {}", seq[1..].escape_ascii()) } } x => { if let Some(dir) = byte_to_dir(x) { Arrow(dir) } else { todo!("escape characters {}", seq[1..].escape_ascii()) } } } } else { if debug { println!("escape: {}", seq.escape_ascii()); } match seq[0] { b'd' => CtrlDeleteRight, x => todo!("unhandled escape code: ESC {x}"), } } } pub fn read(debug: bool) -> KeyboardInput { use KeyboardInput::*; let Some(x) = read1() else { return KeyboardInput::Eof; }; match x { 1 => CtrlA, 2 => CtrlB, 3 => CtrlC, 4 => CtrlD, 5 => CtrlE, 8 | 127 => DeleteLeft, 12 => CtrlL, 18 => CtrlR, 27 => read_escape(debug), b'\t' | b'\r' => Key(x), x if !x.is_ascii_control() => Key(x), x => todo!("unimplemented control code: {x}"), } } struct EscapingStdinReader<'a> { buf: Vec, trie: &'a EscapeTrie, } enum ByteProcessingResult<'a> { Done(KbInput<'a>), Continue(EscapingStdinReader<'a>), } impl<'a> EscapingStdinReader<'a> { pub fn new(trie: &'a EscapeTrie) -> Self { Self { buf: Vec::new(), trie, } } pub fn process_byte(mut self, byte: u8) -> ByteProcessingResult<'a> { match self.trie { EscapeTrie::Done(_) => ByteProcessingResult::Done(KbInput::Key([byte])), EscapeTrie::More(trie) => { self.buf.push(byte); match trie.get(&byte) { Some(EscapeTrie::Done(keys)) => { ByteProcessingResult::Done(KbInput::Escape(Escape { keys: &keys[..], value: self.buf, })) } Some(trie) => { self.trie = trie; ByteProcessingResult::Continue(self) } None => ByteProcessingResult::Done(KbInput::InvalidEscape(self.buf)), } } } } } enum EscapeTrie { Done(Vec<&'static str>), More(BTreeMap), } enum KbInput<'a> { Key([u8; 1]), Escape(Escape<'a>), InvalidEscape(Vec), } impl<'a> KbInput<'a> { pub fn as_bytes(&'a self) -> &'a [u8] { match self { KbInput::Key(x) => &x[..], KbInput::Escape(e) => &e.value[..], KbInput::InvalidEscape(e) => &e[..], } } } struct Escape<'a> { keys: &'a [&'a str], value: Vec, } use terminfo_lean::parse::Terminfo; static TERMINFO: RwLock>> = RwLock::new(None); fn parse_terminfo() -> Result, ()> { let term = std::env::var_os("TERM").unwrap_or_else(|| "xterm".into()); let terminfo_file_path = terminfo_lean::locate::locate(&term) .map_err(|e| println!("failed to locate terminfo file for terminal {term:?}: {e:?}",))?; let mut terminfo_file = std::fs::File::open(&terminfo_file_path).map_err(|e| { println!("failed to open terminfo file at location {terminfo_file_path:?}: {e:?}") })?; let mut buf = Vec::new(); terminfo_file.read_to_end(&mut buf).map_err(|e| { println!("failed to read terminfo file at location {terminfo_file_path:?}: {e:?}") })?; buf.shrink_to_fit(); let terminfo = terminfo_lean::parse::parse(buf.leak()).map_err(|e| { println!("failed to parse terminfo file at location {terminfo_file_path:?}: {e:?}") })?; Ok(terminfo) } fn parse_terminfo_backup() -> Terminfo<'static> { todo!("panic-safe backup terminfo") } pub fn setup() { let ti = parse_terminfo().unwrap_or_else(|_| { println!("using backup terminfo (might not be correct for this terminal)"); parse_terminfo_backup() }); let ti = Box::leak(Box::new(ti)); TERMINFO.clear_poison(); *TERMINFO.write().unwrap() = Some(ti); } pub fn ti() -> &'static Terminfo<'static> { TERMINFO.read().unwrap().unwrap() } fn is_parametrized(x: &[u8]) -> bool { let mut pct = false; for &b in x { if b == b'%' { pct = !pct; } else if pct { return true; } } false } fn trie_from_words(words: Vec<(&'static str, &[u8])>) -> EscapeTrie { let mut tree = BTreeMap::new(); let mut all_empty = true; for (key, val) in words.iter() { if let Some(byte) = val.get(0) { all_empty = false; tree.entry(byte) .or_insert_with(Vec::new) .push((*key, &val[1..])); } } if all_empty { EscapeTrie::Done(words.into_iter().map(|x| x.0).collect()) } else { let trie = tree .into_iter() .map(|(k, v)| (*k, trie_from_words(v))) .collect(); EscapeTrie::More(trie) } } impl From<&'static Terminfo<'static>> for EscapeTrie { fn from(ti: &'static Terminfo<'static>) -> Self { let w: Vec<(&'static str, &'static [u8])> = ti .strings .iter() .filter(|(_, v)| !is_parametrized(v)) .map(|(k, v)| (*k, *v)) .collect(); trie_from_words(w) } }