more work on re-implementing the readline module

This commit is contained in:
2025-06-05 01:48:44 -04:00
parent 2c6577ad06
commit f57cf625ef
10 changed files with 3947 additions and 72 deletions

View File

@@ -1,12 +1,13 @@
use std::{env, fmt::Write, io::{BufRead, BufReader, Read}, ops::{Deref, DerefMut}, os::fd::{AsFd, BorrowedFd, RawFd}};
use std::{env, fmt::{Debug, Write}, io::{BufRead, BufReader, Read}, os::fd::{AsFd, BorrowedFd, RawFd}};
use nix::{errno::Errno, libc, poll::{self, PollFlags, PollTimeout}, unistd::isatty};
use nix::{errno::Errno, libc::{self, STDIN_FILENO}, poll::{self, PollFlags, PollTimeout}, sys::termios, unistd::isatty};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, prompt::readline::keys::{KeyCode, ModKeys}};
use crate::prelude::*;
use super::linebuf::LineBuf;
use super::{keys::KeyEvent, linebuf::LineBuf};
pub type Row = u16;
pub type Col = u16;
@@ -43,7 +44,7 @@ pub fn get_win_size(fd: RawFd) -> (Col,Row) {
} else {
size.ws_row
};
(cols.into(), rows.into())
(cols, rows)
}
_ => (80,24)
}
@@ -182,7 +183,7 @@ pub struct TermBuffer {
impl TermBuffer {
pub fn new(tty: RawFd) -> Self {
assert!(isatty(tty).is_ok_and(|r| r == true));
assert!(isatty(tty).is_ok_and(|r| r));
Self {
tty
}
@@ -191,7 +192,7 @@ impl TermBuffer {
impl Read for TermBuffer {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
assert!(isatty(self.tty).is_ok_and(|r| r == true));
assert!(isatty(self.tty).is_ok_and(|r| r));
loop {
match nix::unistd::read(self.tty, buf) {
Ok(n) => return Ok(n),
@@ -202,10 +203,52 @@ impl Read for TermBuffer {
}
}
pub struct RawModeGuard {
orig: termios::Termios,
fd: RawFd,
}
impl RawModeGuard {
/// Disable raw mode temporarily for a specific operation
pub fn disable_for<F: FnOnce() -> R, R>(&self, func: F) -> R {
unsafe {
let fd = BorrowedFd::borrow_raw(self.fd);
// Temporarily restore the original termios
termios::tcsetattr(fd, termios::SetArg::TCSANOW, &self.orig)
.expect("Failed to temporarily disable raw mode");
// Run the function
let result = func();
// Re-enable raw mode
let mut raw = self.orig.clone();
termios::cfmakeraw(&mut raw);
termios::tcsetattr(fd, termios::SetArg::TCSANOW, &raw)
.expect("Failed to re-enable raw mode");
result
}
}
}
impl Drop for RawModeGuard {
fn drop(&mut self) {
unsafe {
let _ = termios::tcsetattr(BorrowedFd::borrow_raw(self.fd), termios::SetArg::TCSANOW, &self.orig);
}
}
}
pub struct TermReader {
buffer: BufReader<TermBuffer>
}
impl Default for TermReader {
fn default() -> Self {
Self::new()
}
}
impl TermReader {
pub fn new() -> Self {
Self {
@@ -213,8 +256,22 @@ impl TermReader {
}
}
pub fn raw_mode(&self) -> RawModeGuard {
let fd = self.buffer.get_ref().tty;
let orig = termios::tcgetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}).expect("Failed to get terminal attributes");
let mut raw = orig.clone();
termios::cfmakeraw(&mut raw);
termios::tcsetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}, termios::SetArg::TCSANOW, &raw)
.expect("Failed to set terminal to raw mode");
RawModeGuard { orig, fd }
}
/// Execute some logic in raw mode
///
/// Saves the termios before running the given function.
/// If the given function panics, the panic will halt momentarily to restore the termios
pub fn poll(&mut self, timeout: PollTimeout) -> ShResult<bool> {
if self.buffer.buffer().len() > 0 {
if !self.buffer.buffer().is_empty() {
return Ok(true)
}
@@ -234,6 +291,7 @@ impl TermReader {
}
pub fn peek_byte(&mut self) -> std::io::Result<u8> {
flog!(DEBUG,"filling buffer");
let buf = self.buffer.fill_buf()?;
if buf.is_empty() {
Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "EOF"))
@@ -245,6 +303,113 @@ impl TermReader {
pub fn consume_byte(&mut self) {
self.buffer.consume(1);
}
pub fn read_key(&mut self) -> ShResult<KeyEvent> {
use core::str;
let mut collected = Vec::with_capacity(4);
loop {
let byte = self.next_byte()?;
collected.push(byte);
// If it's an escape seq, delegate to ESC sequence handler
if collected[0] == 0x1b && collected.len() == 1 && self.poll(PollTimeout::ZERO)? {
return self.parse_esc_seq();
}
// Try parse as valid UTF-8
if let Ok(s) = str::from_utf8(&collected) {
return Ok(KeyEvent::new(s, ModKeys::empty()));
}
// UTF-8 max 4 bytes — if its invalid at this point, bail
if collected.len() >= 4 {
break;
}
}
Ok(KeyEvent(KeyCode::Null, ModKeys::empty()))
}
pub fn parse_esc_seq(&mut self) -> ShResult<KeyEvent> {
let mut seq = vec![0x1b];
let b1 = self.peek_byte()?;
self.consume_byte();
seq.push(b1);
match b1 {
b'[' => {
let b2 = self.peek_byte()?;
self.consume_byte();
seq.push(b2);
match b2 {
b'A' => Ok(KeyEvent(KeyCode::Up, ModKeys::empty())),
b'B' => Ok(KeyEvent(KeyCode::Down, ModKeys::empty())),
b'C' => Ok(KeyEvent(KeyCode::Right, ModKeys::empty())),
b'D' => Ok(KeyEvent(KeyCode::Left, ModKeys::empty())),
b'1'..=b'9' => {
let mut digits = vec![b2];
loop {
let b = self.peek_byte()?;
seq.push(b);
self.consume_byte();
if b == b'~' || b == b';' {
break;
} else if b.is_ascii_digit() {
digits.push(b);
} else {
break;
}
}
let key = match digits.as_slice() {
[b'1'] => KeyCode::Home,
[b'3'] => KeyCode::Delete,
[b'4'] => KeyCode::End,
[b'5'] => KeyCode::PageUp,
[b'6'] => KeyCode::PageDown,
[b'7'] => KeyCode::Home, // xterm alternate
[b'8'] => KeyCode::End, // xterm alternate
[b'1', b'5'] => KeyCode::F(5),
[b'1', b'7'] => KeyCode::F(6),
[b'1', b'8'] => KeyCode::F(7),
[b'1', b'9'] => KeyCode::F(8),
[b'2', b'0'] => KeyCode::F(9),
[b'2', b'1'] => KeyCode::F(10),
[b'2', b'3'] => KeyCode::F(11),
[b'2', b'4'] => KeyCode::F(12),
_ => KeyCode::Esc,
};
Ok(KeyEvent(key, ModKeys::empty()))
}
_ => Ok(KeyEvent(KeyCode::Esc, ModKeys::empty())),
}
}
b'O' => {
let b2 = self.peek_byte()?;
self.consume_byte();
seq.push(b2);
let key = match b2 {
b'P' => KeyCode::F(1),
b'Q' => KeyCode::F(2),
b'R' => KeyCode::F(3),
b'S' => KeyCode::F(4),
_ => KeyCode::Esc,
};
Ok(KeyEvent(key, ModKeys::empty()))
}
_ => Ok(KeyEvent(KeyCode::Esc, ModKeys::empty())),
}
}
}
impl AsFd for TermReader {
@@ -261,6 +426,15 @@ pub struct Layout {
pub end: Pos
}
impl Debug for Layout {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Layout: ")?;
writeln!(f, "\tPrompt End: {:?}",self.prompt_end)?;
writeln!(f, "\tCursor: {:?}",self.cursor)?;
writeln!(f, "\tEnd: {:?}",self.end)
}
}
impl Layout {
pub fn new() -> Self {
let w_calc = width_calculator();
@@ -273,6 +447,12 @@ impl Layout {
}
}
impl Default for Layout {
fn default() -> Self {
Self::new()
}
}
pub struct LineWriter {
out: RawFd,
t_cols: Col, // terminal width
@@ -297,7 +477,8 @@ impl LineWriter {
write_all(self.out, buf)?;
Ok(())
}
pub fn clear_rows(&mut self, layout: &Layout) {
pub fn clear_rows(&mut self, layout: &Layout) -> ShResult<()> {
self.buffer.clear();
let rows_to_clear = layout.end.row;
let cursor_row = layout.cursor.row;
@@ -307,27 +488,31 @@ impl LineWriter {
}
for _ in 0..rows_to_clear {
self.buffer.push_str("\x1b[K\x1b[A");
self.buffer.push_str("\x1b[2K\x1b[A");
}
self.buffer.push_str("\x1b[K");
}
pub fn move_cursor(&mut self, old: Pos, new: Pos) -> ShResult<()> {
self.buffer.push_str("\x1b[2K");
flog!(DEBUG, self.buffer);
write_all(self.out,self.buffer.as_str())?;
self.buffer.clear();
let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer");
Ok(())
}
pub fn get_cursor_movement(&self, old: Pos, new: Pos) -> ShResult<String> {
let mut buffer = String::new();
let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to cursor movement buffer");
match new.row.cmp(&old.row) {
std::cmp::Ordering::Greater => {
let shift = new.row - old.row;
match shift {
1 => self.buffer.push_str("\x1b[B"),
_ => write!(self.buffer, "\x1b[{shift}B").map_err(err)?
1 => buffer.push_str("\x1b[B"),
_ => write!(buffer, "\x1b[{shift}B").map_err(err)?
}
}
std::cmp::Ordering::Less => {
let shift = old.row - new.row;
match shift {
1 => self.buffer.push_str("\x1b[A"),
_ => write!(self.buffer, "\x1b[{shift}A").map_err(err)?
1 => buffer.push_str("\x1b[A"),
_ => write!(buffer, "\x1b[{shift}A").map_err(err)?
}
}
std::cmp::Ordering::Equal => { /* Do nothing */ }
@@ -337,20 +522,26 @@ impl LineWriter {
std::cmp::Ordering::Greater => {
let shift = new.col - old.col;
match shift {
1 => self.buffer.push_str("\x1b[C"),
_ => write!(self.buffer, "\x1b[{shift}C").map_err(err)?
1 => buffer.push_str("\x1b[C"),
_ => write!(buffer, "\x1b[{shift}C").map_err(err)?
}
}
std::cmp::Ordering::Less => {
let shift = old.col - new.col;
match shift {
1 => self.buffer.push_str("\x1b[D"),
_ => write!(self.buffer, "\x1b[{shift}D").map_err(err)?
1 => buffer.push_str("\x1b[D"),
_ => write!(buffer, "\x1b[{shift}D").map_err(err)?
}
}
std::cmp::Ordering::Equal => { /* Do nothing */ }
}
write_all(self.out, self.buffer.as_str())?;
Ok(buffer)
}
pub fn move_cursor(&mut self, old: Pos, new: Pos) -> ShResult<()> {
self.buffer.clear();
let movement = self.get_cursor_movement(old, new)?;
write_all(self.out, &movement)?;
Ok(())
}
@@ -358,46 +549,37 @@ impl LineWriter {
&mut self,
prompt: &str,
line: &LineBuf,
old_layout: &Layout,
new_layout: &Layout,
) -> ShResult<()> {
let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer");
self.buffer.clear();
self.clear_rows(old_layout);
let end = new_layout.end;
let cursor = new_layout.cursor;
self.buffer.push_str(prompt);
self.buffer.push_str(line.as_str());
if end.col == 0
&& end.row > 0
{
if end.col == 0 && end.row > 0 {
// The line has wrapped. We need to use our own line break.
self.buffer.push('\n')
self.buffer.push('\n');
}
let cursor_row_offset = end.row - cursor.row;
match cursor_row_offset {
0 => { /* Do nothing */ }
1 => self.buffer.push_str("\x1b[A"),
_ => write!(self.buffer, "\x1b[{cursor_row_offset}A").map_err(err)?
}
let cursor_col = cursor.col;
match cursor_col {
0 => self.buffer.push('\r'),
1 => self.buffer.push_str("\x1b[C"),
_ => write!(self.buffer, "\x1b[{cursor_col}C").map_err(err)?
}
let movement = self.get_cursor_movement(end, cursor)?;
write!(self.buffer, "{}", &movement).map_err(err)?;
write_all(self.out, self.buffer.as_str())?;
Ok(())
}
pub fn get_layout_from_parts(&mut self, prompt: &str, to_cursor: &str, to_end: &str) -> Layout {
self.update_t_cols();
let prompt_end = self.calc_pos(prompt, Pos { col: 0, row: 0 });
let cursor = self.calc_pos(to_cursor, prompt_end);
let end = self.calc_pos(to_end, prompt_end);
Layout { w_calc: width_calculator(), prompt_end, cursor, end }
}
pub fn calc_pos(&self, s: &str, orig: Pos) -> Pos {
let mut pos = orig;
let mut esc_seq = 0;
@@ -417,7 +599,7 @@ impl LineWriter {
pos.col = c_width;
}
}
if pos.col > self.t_cols {
if pos.col >= self.t_cols {
pos.row += 1;
pos.col = 0;
}
@@ -430,32 +612,47 @@ impl LineWriter {
self.t_cols = t_cols;
}
pub fn move_cursor_at_leftmost(&mut self, rdr: &mut TermReader) -> ShResult<()> {
if rdr.poll(PollTimeout::ZERO)? {
pub fn move_cursor_at_leftmost(&mut self, rdr: &mut TermReader, use_newline: bool) -> ShResult<()> {
let result = rdr.poll(PollTimeout::ZERO)?;
if result {
// The terminals reply is going to be stuck behind the currently buffered output
// So let's get out of here
return Ok(())
}
// Ping the cursor's position
self.flush_write("\x1b[6n")?;
self.flush_write("\x1b[6n\n")?;
// Excessively paranoid invariant checking
if !rdr.poll(PollTimeout::from(100u8))?
|| rdr.next_byte()? as char != '\x1b'
|| rdr.next_byte()? as char != '['
|| read_digits_until(rdr, ';')?.is_none() {
// Invariant is broken, get out
if !rdr.poll(PollTimeout::from(255u8))? {
return Ok(())
}
if rdr.next_byte()? as char != '\x1b' {
return Ok(())
}
if rdr.next_byte()? as char != '[' {
return Ok(())
}
if read_digits_until(rdr, ';')?.is_none() {
return Ok(())
}
// We just consumed everything up to the column number, so let's get that now
let col = read_digits_until(rdr, 'R')?;
// The cursor is not at the leftmost, so let's fix that
if col != Some(1) {
// We use '\n' instead of '\r' because if there's a bunch of garbage on this line,
// It might pollute the prompt/line buffer if those are shorter than said garbage
self.flush_write("\n")?;
if use_newline {
// We use '\n' instead of '\r' sometimes because if there's a bunch of garbage on this line,
// It might pollute the prompt/line buffer if those are shorter than said garbage
self.flush_write("\n")?;
} else {
// Sometimes though, we know that there's nothing to the right of the cursor after moving
// So we just move to the left.
self.flush_write("\r")?;
}
}
Ok(())