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.rs128
1 files changed, 107 insertions, 21 deletions
diff --git a/src/ansi/mod.rs b/src/ansi/mod.rs
index d7e2ab3..96fb87a 100644
--- a/src/ansi/mod.rs
+++ b/src/ansi/mod.rs
@@ -1,4 +1,8 @@
-use std::{collections::BTreeMap, io::Read, sync::RwLock};
+use std::{
+ collections::{BTreeMap, VecDeque},
+ io::{Read, Write},
+ sync::RwLock,
+};
pub mod colors;
@@ -10,32 +14,112 @@ fn read1() -> Option<u8> {
}
}
-pub fn read(debug: bool) -> Option<KbInput<'static>> {
- let mut reader = EscapingStdinReader::new(et());
- loop {
- let Some(b) = read1() else {
- break None;
- };
- match reader.process_byte(b) {
- ByteProcessingResult::Done(kb) => {
- if debug {
- if let KbInput::Escape(e) = &kb {
- for k in e.keys.iter() {
- print!("{k} ")
+pub struct TerminalInput {
+ pub debug: bool,
+ buf: VecDeque<KbInput<'static>>,
+}
+
+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());
}
- println!("{}\r", kb.as_bytes().escape_ascii());
- }
- if let KbInput::InvalidEscape(x) = &kb
- && x.len() == 1
- {
- break Some(KbInput::Key([x[0]]));
+
+ if let KbInput::InvalidEscape(x) = &kb
+ && x.len() == 1
+ {
+ self.buf.push_back(KbInput::Key([x[0]]));
+ } else {
+ self.buf.push_back(kb);
+ }
+ return;
}
- break Some(kb);
+ ByteProcessingResult::Continue(r) => reader = r,
}
- ByteProcessingResult::Continue(r) => reader = r,
}
}
+
+ pub fn read(&mut self) -> Option<KbInput<'static>> {
+ 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<CursorPos> {
+ 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<CursorPos> {
+ 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::<usize>().ok()?;
+ let col = second.parse::<usize>().ok()?;
+
+ Some(CursorPos { row, col })
}
struct EscapingStdinReader<'a> {
@@ -109,6 +193,8 @@ pub struct Escape<'a> {
use terminfo_lean::parse::Terminfo;
+use crate::cursor::CursorPos;
+
static TERMINFO: RwLock<Option<&'static Terminfo<'static>>> = RwLock::new(None);
static ESCAPE_TRIE: RwLock<Option<&'static EscapeTrie>> = RwLock::new(None);