aboutsummaryrefslogtreecommitdiffstats
path: root/src/ansi/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/ansi/mod.rs')
-rw-r--r--src/ansi/mod.rs293
1 files changed, 293 insertions, 0 deletions
diff --git a/src/ansi/mod.rs b/src/ansi/mod.rs
new file mode 100644
index 0000000..4fc550b
--- /dev/null
+++ b/src/ansi/mod.rs
@@ -0,0 +1,293 @@
+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<u8> {
+ 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<Direction> {
+ 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<u8>,
+ 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<u8, EscapeTrie>),
+}
+
+enum KbInput<'a> {
+ Key([u8; 1]),
+ Escape(Escape<'a>),
+ InvalidEscape(Vec<u8>),
+}
+
+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<u8>,
+}
+
+use terminfo_lean::parse::Terminfo;
+
+static TERMINFO: RwLock<Option<&'static Terminfo<'static>>> = RwLock::new(None);
+
+fn parse_terminfo() -> Result<Terminfo<'static>, ()> {
+ 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)
+ }
+}