diff options
| author | Jonas Maier <> | 2026-05-24 12:52:44 +0200 |
|---|---|---|
| committer | Jonas Maier <> | 2026-05-24 12:52:44 +0200 |
| commit | 183aee63b56be098efad606df9cf39ae637c4117 (patch) | |
| tree | a80f65eca7368e3999e1f880e928b0bdb685d2ef | |
| parent | 36cbfcaafcb8a3b1c47650439531a35c99b203ea (diff) | |
| download | pish-183aee63b56be098efad606df9cf39ae637c4117.tar.gz | |
add logic to query cursor position
| -rw-r--r-- | src/ansi/mod.rs | 128 | ||||
| -rw-r--r-- | src/lib.rs | 14 | ||||
| -rw-r--r-- | src/run/builtin.rs | 19 |
3 files changed, 128 insertions, 33 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); @@ -1,7 +1,8 @@ #![feature( unix_socket_ancillary_data, peer_credentials_unix_socket, - associated_type_defaults + associated_type_defaults, + slice_split_once )] #![allow(clippy::needless_range_loop)] @@ -112,14 +113,14 @@ pub struct Session { /// byte literals to command invocation ascii_keybinds: HashMap<BString, parse::Command<PostExpansion>>, - debug_keystrokes: bool, - /// n before end of history.len() /// 0 == not checking history history_visit: usize, highlighter: syntax_highlighting::Highlighter, prompt: Prompt, + + terminal_input: Option<ansi::TerminalInput>, } impl Session { @@ -140,11 +141,11 @@ impl Session { ctrlc: Default::default(), ti_keybinds: HashMap::new(), ascii_keybinds: HashMap::new(), - debug_keystrokes: false, history_visit: 0, highlighter: syntax_highlighting::Highlighter::new(), prompt, vars, + terminal_input: None, } } } @@ -390,6 +391,7 @@ pub fn event_loop() { socket_running: None, path_cache: Default::default(), ctrlc: Default::default(), + terminal_input: Some(ansi::TerminalInput::new()), ..Session::new_noninteractive() }; @@ -408,11 +410,11 @@ pub fn event_loop() { 'repl: loop { let mut se = session.lock().unwrap(); - let Some(key) = ansi::read(se.debug_keystrokes) else { + let Some(key) = se.terminal_input.as_mut().unwrap().read() else { break; }; - if se.debug_keystrokes { + if se.terminal_input.as_mut().unwrap().debug { println!("{key:?}"); } diff --git a/src/run/builtin.rs b/src/run/builtin.rs index dfafcbe..fab7565 100644 --- a/src/run/builtin.rs +++ b/src/run/builtin.rs @@ -622,12 +622,15 @@ impl Builtin for bind { "bind" } - fn special(&mut self, _session: Arc<Mutex<Session>>, args: &[BString]) { + fn special(&mut self, session: Arc<Mutex<Session>>, args: &[BString]) { if Self::is_interactive(args) { - let raw = crate::raw::ScopedRawMode::on_fd(0); - raw.enable(); - self.key = crate::ansi::read(false); - raw.disable(); + let mut se = session.lock().unwrap(); + if let Some(ti) = se.terminal_input.as_mut() { + let raw = crate::raw::ScopedRawMode::on_fd(0); + raw.enable(); + self.key = ti.read(); + raw.disable(); + } } } @@ -1056,7 +1059,11 @@ mod dbg { let mut se = session.lock().unwrap(); for arg in args { match &arg[..] { - b"keys" | b"keystrokes" => se.debug_keystrokes = !se.debug_keystrokes, + b"keys" | b"keystrokes" => { + if let Some(ti) = se.terminal_input.as_mut() { + ti.debug = !ti.debug; + } + } _ => { stdout.write_all(b"debug: unknown option ")?; stdout.write_all(&arg)?; |
