aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJonas Maier <>2026-04-21 19:59:02 +0200
committerJonas Maier <>2026-04-21 19:59:02 +0200
commita9bf864158e67353f57047cdc4d6b0e325d73eae (patch)
treead6a1845ee3f695f066ba0caa0d01b87e86c8146 /src
parenta8e3dcf526462d82402985d137402963b0b344de (diff)
downloadpish-a9bf864158e67353f57047cdc4d6b0e325d73eae.tar.gz
maybe soon better escape code parsing
Diffstat (limited to 'src')
-rw-r--r--src/ansi.rs137
-rw-r--r--src/ansi/mod.rs293
-rw-r--r--src/main.rs1
-rw-r--r--src/run/builtin.rs37
-rw-r--r--src/run/mod.rs1
5 files changed, 332 insertions, 137 deletions
diff --git a/src/ansi.rs b/src/ansi.rs
deleted file mode 100644
index 522c752..0000000
--- a/src/ansi.rs
+++ /dev/null
@@ -1,137 +0,0 @@
-use std::io::Read;
-
-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,
- 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}"),
- }
-}
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)
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 742ed9a..d388ce1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -308,6 +308,7 @@ fn exec_rc_file(se: Arc<Mutex<Session>>) {
fn event_loop() {
history::setup();
+ ansi::setup();
let stdin = io::stdin();
diff --git a/src/run/builtin.rs b/src/run/builtin.rs
index c080c93..f5dff81 100644
--- a/src/run/builtin.rs
+++ b/src/run/builtin.rs
@@ -564,3 +564,40 @@ impl Builtin for debug {
Ok(())
}
}
+
+pub struct terminfo;
+impl Builtin for terminfo {
+ fn name(&self) -> &str {
+ "terminfo"
+ }
+
+ fn io(
+ &self,
+ _session: Arc<Mutex<Session>>,
+ _args: &[BString],
+ _stdin: &mut dyn Read,
+ f: &mut dyn Write,
+ ) -> Result {
+ let ti = crate::ansi::ti();
+
+ writeln!(f, "# Booleans")?;
+ for k in ti.booleans.iter() {
+ writeln!(f, "{k}")?;
+ }
+ writeln!(f)?;
+
+ writeln!(f, "# Numbers")?;
+ for (k, v) in ti.numbers.iter() {
+ writeln!(f, "{k} {v}")?;
+ }
+ writeln!(f)?;
+
+ writeln!(f, "# Strings")?;
+ for (k, v) in ti.strings.iter() {
+ writeln!(f, "{k} {}", v.escape_ascii())?;
+ }
+ writeln!(f)?;
+
+ Ok(())
+ }
+}
diff --git a/src/run/mod.rs b/src/run/mod.rs
index 8a728c5..c866c6e 100644
--- a/src/run/mod.rs
+++ b/src/run/mod.rs
@@ -549,6 +549,7 @@ const BUILTINS: &[&'static dyn Builtin] = &[
&builtin::unalias,
#[cfg(debug_assertions)]
&builtin::debug,
+ &builtin::terminfo,
];
pub fn builtin_map() -> HashMap<BString, &'static dyn Builtin> {