use std::{ collections::{BTreeMap, VecDeque}, io::{Read, Write}, sync::RwLock, }; pub mod colors; fn read1() -> Option { let mut buf = [0]; match std::io::stdin().lock().read_exact(&mut buf) { Ok(_) => Some(buf[0]), Err(_) => None, } } pub struct TerminalInput { pub debug: bool, buf: VecDeque>, } impl TerminalInput { pub fn new() -> Self { Self { debug: false, buf: VecDeque::new(), } } fn read_internal(&mut self) { let mut reader = EscapingStdinReader::new(et()); loop { let Some(b) = read1() else { // EOF return; }; match reader.process_byte(b) { ByteProcessingResult::Done(kb) => { if self.debug { if let KbInput::Escape(e) = &kb { for k in e.keys.iter() { print!("{k} ") } } println!("{}\r", kb.as_bytes().escape_ascii()); } if let KbInput::InvalidEscape(x) = &kb && x.len() == 1 { self.buf.push_back(KbInput::Key([x[0]])); } else { self.buf.push_back(kb); } return; } ByteProcessingResult::Continue(r) => reader = r, } } } pub fn read(&mut self) -> Option> { if self.buf.is_empty() { self.read_internal(); } self.buf.pop_front() } fn read_all_input(&mut self) { while { let buf_len = self.buf.len(); self.read_internal(); self.buf.len() != buf_len } {} } pub fn query_cursor_position(&mut self) -> Option { self.read_all_input(); // query to request cursor position let mut stdout = std::io::stdout().lock(); stdout.write_all(b"\x1b[6n").ok()?; stdout.flush().ok()?; drop(stdout); loop { let buf_len = self.buf.len(); self.read_internal(); // no new input, give up if self.buf.len() <= buf_len { return None; } // buf.len() > buf_len >= 0, unwrap is safe. let input = self.buf.back().unwrap(); if let Some(pos) = try_parse_cursor_position_message(input.as_bytes()) { self.buf.pop_back(); return Some(pos); } } } } // expecting a string of the form `\x1b[n;mR` fn try_parse_cursor_position_message(mut msg: &[u8]) -> Option { if !msg.starts_with(b"\x1b[") || !msg.ends_with(b"R") { return None; } msg = &msg[2..msg.len() - 1]; let (first, second) = msg.split_once(|x| *x == b';')?; let first = str::from_utf8(first).ok()?; let second = str::from_utf8(second).ok()?; let row = first.parse::().ok()?; let col = second.parse::().ok()?; Some(CursorPos { row, col }) } 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), } #[derive(Clone, Debug)] pub 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[..], } } } #[derive(Clone, Debug)] pub struct Escape<'a> { pub keys: &'a [&'a str], pub value: Vec, } use terminfo_lean::parse::Terminfo; use crate::cursor::CursorPos; static TERMINFO: RwLock>> = RwLock::new(None); static ESCAPE_TRIE: 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: &'static _ = Box::leak(Box::new(ti)); TERMINFO.clear_poison(); *TERMINFO.write().unwrap() = Some(ti); let et = Box::leak(Box::new(EscapeTrie::from(ti))); ESCAPE_TRIE.clear_poison(); *ESCAPE_TRIE.write().unwrap() = Some(et); } pub fn ti() -> &'static Terminfo<'static> { TERMINFO.read().unwrap().unwrap() } fn et() -> &'static EscapeTrie { ESCAPE_TRIE.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.first() { 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<&Terminfo<'static>> for EscapeTrie { fn from(ti: &Terminfo<'static>) -> Self { let w: Vec<(&'static str, &'static [u8])> = ti .strings .iter() .filter(|(k, v)| k.starts_with("k") && !is_parametrized(v)) .map(|(k, v)| (*k, *v)) .collect(); trie_from_words(w) } }