From 183aee63b56be098efad606df9cf39ae637c4117 Mon Sep 17 00:00:00 2001 From: Jonas Maier <> Date: Sun, 24 May 2026 12:52:44 +0200 Subject: add logic to query cursor position --- src/ansi/mod.rs | 128 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 107 insertions(+), 21 deletions(-) (limited to 'src/ansi') 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 { } } -pub fn read(debug: bool) -> Option> { - 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>, +} + +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> { + 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> { @@ -109,6 +193,8 @@ pub struct Escape<'a> { use terminfo_lean::parse::Terminfo; +use crate::cursor::CursorPos; + static TERMINFO: RwLock>> = RwLock::new(None); static ESCAPE_TRIE: RwLock> = RwLock::new(None); -- cgit v1.2.3