aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJonas Maier <>2026-05-24 12:52:44 +0200
committerJonas Maier <>2026-05-24 12:52:44 +0200
commit183aee63b56be098efad606df9cf39ae637c4117 (patch)
treea80f65eca7368e3999e1f880e928b0bdb685d2ef /src
parent36cbfcaafcb8a3b1c47650439531a35c99b203ea (diff)
downloadpish-183aee63b56be098efad606df9cf39ae637c4117.tar.gz
add logic to query cursor position
Diffstat (limited to 'src')
-rw-r--r--src/ansi/mod.rs128
-rw-r--r--src/lib.rs14
-rw-r--r--src/run/builtin.rs19
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);
diff --git a/src/lib.rs b/src/lib.rs
index c2b4a4a..05c8479 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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)?;