copied rustyline's homework
This commit is contained in:
@@ -1,459 +1,463 @@
|
||||
use std::{os::fd::{BorrowedFd, RawFd}, thread::sleep, time::{Duration, Instant}};
|
||||
use nix::{errno::Errno, fcntl::{fcntl, FcntlArg, OFlag}, libc::{self, STDIN_FILENO}, sys::termios, unistd::{isatty, read, write}};
|
||||
use nix::libc::{winsize, TIOCGWINSZ};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
use std::mem::zeroed;
|
||||
use std::io;
|
||||
use std::{env, fmt::Write, io::{BufRead, BufReader, Read}, ops::{Deref, DerefMut}, os::fd::{AsFd, BorrowedFd, RawFd}};
|
||||
|
||||
use crate::libsh::error::ShResult;
|
||||
use crate::prelude::*;
|
||||
use nix::{errno::Errno, libc, poll::{self, PollFlags, PollTimeout}, unistd::isatty};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
|
||||
use super::keys::{KeyCode, KeyEvent, ModKeys};
|
||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||
|
||||
#[derive(Default,Debug)]
|
||||
struct WriteMap {
|
||||
lines: usize,
|
||||
cols: usize,
|
||||
offset: usize
|
||||
use super::linebuf::LineBuf;
|
||||
|
||||
pub type Row = u16;
|
||||
pub type Col = u16;
|
||||
|
||||
#[derive(Default,Clone,Copy,PartialEq,Eq,PartialOrd,Ord,Debug)]
|
||||
pub struct Pos {
|
||||
col: Col,
|
||||
row: Row
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Terminal {
|
||||
stdin: RawFd,
|
||||
stdout: RawFd,
|
||||
recording: bool,
|
||||
write_records: WriteMap,
|
||||
cursor_records: WriteMap
|
||||
// I'd like to thank rustyline for this idea
|
||||
nix::ioctl_read_bad!(win_size, libc::TIOCGWINSZ, libc::winsize);
|
||||
|
||||
pub fn get_win_size(fd: RawFd) -> (Col,Row) {
|
||||
use std::mem::zeroed;
|
||||
|
||||
if cfg!(test) {
|
||||
return (80,24)
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let mut size: libc::winsize = zeroed();
|
||||
match win_size(fd, &mut size) {
|
||||
Ok(0) => {
|
||||
/* rustyline code says:
|
||||
In linux pseudo-terminals are created with dimensions of
|
||||
zero. If host application didn't initialize the correct
|
||||
size before start we treat zero size as 80 columns and
|
||||
infinite rows
|
||||
*/
|
||||
let cols = if size.ws_col == 0 { 80 } else { size.ws_col };
|
||||
let rows = if size.ws_row == 0 {
|
||||
u16::MAX
|
||||
} else {
|
||||
size.ws_row
|
||||
};
|
||||
(cols.into(), rows.into())
|
||||
}
|
||||
_ => (80,24)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Terminal {
|
||||
pub fn new() -> Self {
|
||||
assert!(isatty(STDIN_FILENO).unwrap());
|
||||
Self {
|
||||
stdin: STDIN_FILENO,
|
||||
stdout: 1,
|
||||
recording: false,
|
||||
// Records for buffer writes
|
||||
// Used to find the start of the buffer
|
||||
write_records: WriteMap::default(),
|
||||
// Records for cursor movements after writes
|
||||
// Used to find the end of the buffer
|
||||
cursor_records: WriteMap::default(),
|
||||
fn write_all(fd: RawFd, buf: &str) -> nix::Result<()> {
|
||||
let mut bytes = buf.as_bytes();
|
||||
while !bytes.is_empty() {
|
||||
match nix::unistd::write(unsafe { BorrowedFd::borrow_raw(fd) }, bytes) {
|
||||
Ok(0) => return Err(Errno::EIO),
|
||||
Ok(n) => bytes = &bytes[n..],
|
||||
Err(Errno::EINTR) => {}
|
||||
Err(r) => return Err(r),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn raw_mode() -> termios::Termios {
|
||||
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");
|
||||
orig
|
||||
}
|
||||
|
||||
pub fn restore_termios(termios: termios::Termios) {
|
||||
termios::tcsetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}, termios::SetArg::TCSANOW, &termios)
|
||||
.expect("Failed to restore terminal settings");
|
||||
}
|
||||
|
||||
|
||||
pub fn get_dimensions(&self) -> ShResult<(usize, usize)> {
|
||||
if !isatty(self.stdin).unwrap_or(false) {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "Not a TTY"))?;
|
||||
}
|
||||
|
||||
let mut ws: winsize = unsafe { zeroed() };
|
||||
|
||||
let res = unsafe { libc::ioctl(self.stdin, TIOCGWINSZ, &mut ws) };
|
||||
if res == -1 {
|
||||
return Err(io::Error::last_os_error())?;
|
||||
}
|
||||
|
||||
Ok((ws.ws_row as usize, ws.ws_col as usize))
|
||||
}
|
||||
|
||||
pub fn start_recording(&mut self, offset: usize) {
|
||||
self.recording = true;
|
||||
self.write_records.offset = offset;
|
||||
}
|
||||
|
||||
pub fn stop_recording(&mut self) {
|
||||
self.recording = false;
|
||||
}
|
||||
|
||||
pub fn save_cursor_pos(&mut self) {
|
||||
self.write("\x1b[s")
|
||||
}
|
||||
|
||||
pub fn restore_cursor_pos(&mut self) {
|
||||
self.write("\x1b[u")
|
||||
}
|
||||
|
||||
pub fn move_cursor_to(&mut self, (row,col): (usize,usize)) {
|
||||
self.write(&format!("\x1b[{row};{col}H",))
|
||||
}
|
||||
|
||||
pub fn with_raw_mode<F: FnOnce() -> R, R>(func: F) -> R {
|
||||
let saved = Self::raw_mode();
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(func));
|
||||
Self::restore_termios(saved);
|
||||
match result {
|
||||
Ok(r) => r,
|
||||
Err(e) => std::panic::resume_unwind(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_byte(&self, buf: &mut [u8]) -> usize {
|
||||
Self::with_raw_mode(|| {
|
||||
read(self.stdin, buf).expect("Failed to read from stdin")
|
||||
})
|
||||
}
|
||||
|
||||
fn read_blocks_then_read(&self, buf: &mut [u8], timeout: Duration) -> Option<usize> {
|
||||
Self::with_raw_mode(|| {
|
||||
self.read_blocks(false);
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
match read(self.stdin, buf) {
|
||||
Ok(n) if n > 0 => {
|
||||
self.read_blocks(true);
|
||||
return Some(n);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) if e == Errno::EAGAIN => {}
|
||||
Err(_) => return None,
|
||||
}
|
||||
if start.elapsed() > timeout {
|
||||
self.read_blocks(true);
|
||||
return None;
|
||||
}
|
||||
sleep(Duration::from_millis(1));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Same as read_byte(), only non-blocking with a very short timeout
|
||||
pub fn peek_byte(&self, buf: &mut [u8]) -> usize {
|
||||
const TIMEOUT_DUR: Duration = Duration::from_millis(50);
|
||||
Self::with_raw_mode(|| {
|
||||
self.read_blocks(false);
|
||||
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
match read(self.stdin, buf) {
|
||||
Ok(n) if n > 0 => {
|
||||
self.read_blocks(true);
|
||||
return n
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(Errno::EAGAIN) => {}
|
||||
Err(e) => panic!("nonblocking read failed: {e}")
|
||||
}
|
||||
|
||||
if start.elapsed() >= TIMEOUT_DUR {
|
||||
self.read_blocks(true);
|
||||
return 0
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(1));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_blocks(&self, yn: bool) {
|
||||
let flags = OFlag::from_bits_truncate(fcntl(self.stdin, FcntlArg::F_GETFL).unwrap());
|
||||
let new_flags = if !yn {
|
||||
flags | OFlag::O_NONBLOCK
|
||||
// Big credit to rustyline for this
|
||||
fn width(s: &str, esc_seq: &mut u8) -> u16 {
|
||||
let w_calc = width_calculator();
|
||||
if *esc_seq == 1 {
|
||||
if s == "[" {
|
||||
// CSI
|
||||
*esc_seq = 2;
|
||||
} else {
|
||||
flags & !OFlag::O_NONBLOCK
|
||||
};
|
||||
fcntl(self.stdin, FcntlArg::F_SETFL(new_flags)).unwrap();
|
||||
}
|
||||
|
||||
pub fn reset_records(&mut self) {
|
||||
self.write_records = Default::default();
|
||||
self.cursor_records = Default::default();
|
||||
}
|
||||
|
||||
pub fn recorded_write(&mut self, buf: &str, offset: usize) -> ShResult<()> {
|
||||
self.start_recording(offset);
|
||||
self.write(buf);
|
||||
self.stop_recording();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rewinds terminal writing, clears lines and lands on the anchor point of the prompt
|
||||
pub fn unwrite(&mut self) -> ShResult<()> {
|
||||
self.unposition_cursor()?;
|
||||
let WriteMap { lines, cols, offset } = self.write_records;
|
||||
for _ in 0..lines {
|
||||
self.write_unrecorded("\x1b[2K\x1b[A")
|
||||
// two-character sequence
|
||||
*esc_seq = 0;
|
||||
}
|
||||
let col = offset;
|
||||
self.write_unrecorded(&format!("\x1b[{col}G\x1b[0K"));
|
||||
self.reset_records();
|
||||
Ok(())
|
||||
0
|
||||
} else if *esc_seq == 2 {
|
||||
if s == ";" || (s.as_bytes()[0] >= b'0' && s.as_bytes()[0] <= b'9') {
|
||||
/*} else if s == "m" {
|
||||
// last
|
||||
*esc_seq = 0;*/
|
||||
} else {
|
||||
// not supported
|
||||
*esc_seq = 0;
|
||||
}
|
||||
|
||||
pub fn position_cursor(&mut self, (lines,col): (usize,usize)) -> ShResult<()> {
|
||||
flog!(DEBUG,lines);
|
||||
flog!(DEBUG,col);
|
||||
self.cursor_records.lines = lines;
|
||||
self.cursor_records.cols = col;
|
||||
self.cursor_records.offset = self.cursor_pos().1;
|
||||
|
||||
for _ in 0..lines {
|
||||
self.write_unrecorded("\x1b[A")
|
||||
}
|
||||
|
||||
let (_, width) = self.get_dimensions().unwrap();
|
||||
// holy hack spongebob
|
||||
// basically if we've written to the edge of the terminal
|
||||
// and the cursor is at term_width + 1 (column 1 on the next line)
|
||||
// then we are going to manually write a newline
|
||||
// to position the cursor correctly
|
||||
if self.write_records.cols == width && self.cursor_records.cols == 1 {
|
||||
self.cursor_records.lines += 1;
|
||||
self.write_records.lines += 1;
|
||||
self.cursor_records.cols = 1;
|
||||
self.write_records.cols = 1;
|
||||
write(unsafe { BorrowedFd::borrow_raw(self.stdout) }, b"\n").expect("Failed to write to stdout");
|
||||
}
|
||||
|
||||
self.write_unrecorded(&format!("\x1b[{col}G"));
|
||||
|
||||
Ok(())
|
||||
0
|
||||
} else if s == "\x1b" {
|
||||
*esc_seq = 1;
|
||||
0
|
||||
} else if s == "\n" {
|
||||
0
|
||||
} else {
|
||||
w_calc.width(s) as u16
|
||||
}
|
||||
}
|
||||
|
||||
/// Rewinds cursor positioning, lands on the end of the buffer
|
||||
pub fn unposition_cursor(&mut self) ->ShResult<()> {
|
||||
let WriteMap { lines, cols, offset } = self.cursor_records;
|
||||
|
||||
for _ in 0..lines {
|
||||
self.write_unrecorded("\x1b[B")
|
||||
}
|
||||
|
||||
self.write_unrecorded(&format!("\x1b[{offset}G"));
|
||||
|
||||
Ok(())
|
||||
pub fn width_calculator() -> Box<dyn WidthCalculator> {
|
||||
match env::var("TERM_PROGRAM").as_deref() {
|
||||
Ok("Apple_Terminal") => Box::new(UnicodeWidth),
|
||||
Ok("iTerm.app") => Box::new(UnicodeWidth),
|
||||
Ok("WezTerm") => Box::new(UnicodeWidth),
|
||||
Err(std::env::VarError::NotPresent) => match std::env::var("TERM").as_deref() {
|
||||
Ok("xterm-kitty") => Box::new(NoZwj),
|
||||
_ => Box::new(WcWidth)
|
||||
},
|
||||
_ => Box::new(WcWidth)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_bytes(&mut self, buf: &[u8], record: bool) {
|
||||
if self.recording && record { // The function parameter allows us to make sneaky writes while the terminal is recording
|
||||
let (_, width) = self.get_dimensions().unwrap();
|
||||
let mut bytes = buf.iter().map(|&b| b as char).peekable();
|
||||
while let Some(ch) = bytes.next() {
|
||||
match ch {
|
||||
'\n' => {
|
||||
self.write_records.lines += 1;
|
||||
self.write_records.cols = 0;
|
||||
}
|
||||
'\r' => {
|
||||
self.write_records.cols = 0;
|
||||
}
|
||||
// Consume escape sequences
|
||||
'\x1b' if bytes.peek() == Some(&'[') => {
|
||||
bytes.next();
|
||||
while let Some(&ch) = bytes.peek() {
|
||||
if ch.is_ascii_alphabetic() {
|
||||
bytes.next();
|
||||
break
|
||||
} else {
|
||||
bytes.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
'\t' => {
|
||||
let tab_size = 8;
|
||||
let next_tab = tab_size - (self.write_records.cols % tab_size);
|
||||
self.write_records.cols += next_tab;
|
||||
if self.write_records.cols > width {
|
||||
self.write_records.lines += 1;
|
||||
self.write_records.cols = 0;
|
||||
}
|
||||
}
|
||||
_ if ch.is_control() => {
|
||||
// ignore control characters for visual width
|
||||
}
|
||||
_ => {
|
||||
let ch_width = ch.width().unwrap_or(0);
|
||||
if self.write_records.cols + ch_width > width {
|
||||
flog!(DEBUG,ch_width,self.write_records.cols,width,self.write_records.lines);
|
||||
self.write_records.lines += 1;
|
||||
self.write_records.cols = ch_width;
|
||||
}
|
||||
self.write_records.cols += ch_width;
|
||||
}
|
||||
}
|
||||
}
|
||||
flog!(DEBUG,self.write_records.cols);
|
||||
}
|
||||
write(unsafe { BorrowedFd::borrow_raw(self.stdout) }, buf).expect("Failed to write to stdout");
|
||||
}
|
||||
|
||||
|
||||
pub fn write(&mut self, s: &str) {
|
||||
self.write_bytes(s.as_bytes(), true);
|
||||
}
|
||||
|
||||
pub fn write_unrecorded(&mut self, s: &str) {
|
||||
self.write_bytes(s.as_bytes(), false);
|
||||
}
|
||||
|
||||
pub fn writeln(&mut self, s: &str) {
|
||||
self.write(s);
|
||||
self.write_bytes(b"\n", true);
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.write_bytes(b"\x1b[2J\x1b[H", false);
|
||||
}
|
||||
|
||||
pub fn read_key(&self) -> KeyEvent {
|
||||
use core::str;
|
||||
|
||||
let mut buf = [0u8; 8];
|
||||
let mut collected = Vec::with_capacity(5);
|
||||
|
||||
loop {
|
||||
let n = self.read_byte(&mut buf[..1]); // Read one byte at a time
|
||||
if n == 0 {
|
||||
fn read_digits_until(rdr: &mut TermReader, sep: char) -> ShResult<Option<u32>> {
|
||||
let mut num: u32 = 0;
|
||||
loop {
|
||||
match rdr.next_byte()? as char {
|
||||
digit @ '0'..='9' => {
|
||||
let digit = digit.to_digit(10).unwrap();
|
||||
num = append_digit(num, digit);
|
||||
continue;
|
||||
}
|
||||
collected.push(buf[0]);
|
||||
c if c == sep => break,
|
||||
_ => return Ok(None),
|
||||
}
|
||||
}
|
||||
Ok(Some(num))
|
||||
}
|
||||
|
||||
// ESC sequences
|
||||
if collected[0] == 0x1b && collected.len() == 1 {
|
||||
if let Some(code) = self.parse_esc_seq(&mut buf) {
|
||||
return code
|
||||
pub fn append_digit(left: u32, right: u32) -> u32 {
|
||||
left.saturating_mul(10)
|
||||
.saturating_add(right)
|
||||
}
|
||||
|
||||
|
||||
pub trait WidthCalculator {
|
||||
fn width(&self, text: &str) -> usize;
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct UnicodeWidth;
|
||||
|
||||
impl WidthCalculator for UnicodeWidth {
|
||||
fn width(&self, text: &str) -> usize {
|
||||
text.width()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct WcWidth;
|
||||
|
||||
impl WcWidth {
|
||||
pub fn cwidth(&self, ch: char) -> usize {
|
||||
ch.width().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl WidthCalculator for WcWidth {
|
||||
fn width(&self, text: &str) -> usize {
|
||||
let mut width = 0;
|
||||
for ch in text.chars() {
|
||||
width += self.cwidth(ch)
|
||||
}
|
||||
width
|
||||
}
|
||||
}
|
||||
|
||||
const ZWJ: char = '\u{200D}';
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct NoZwj;
|
||||
|
||||
impl WidthCalculator for NoZwj {
|
||||
fn width(&self, text: &str) -> usize {
|
||||
let mut width = 0;
|
||||
for slice in text.split(ZWJ) {
|
||||
width += UnicodeWidth.width(slice);
|
||||
}
|
||||
width
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TermBuffer {
|
||||
tty: RawFd
|
||||
}
|
||||
|
||||
impl TermBuffer {
|
||||
pub fn new(tty: RawFd) -> Self {
|
||||
assert!(isatty(tty).is_ok_and(|r| r == true));
|
||||
Self {
|
||||
tty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
loop {
|
||||
match nix::unistd::read(self.tty, buf) {
|
||||
Ok(n) => return Ok(n),
|
||||
Err(Errno::EINTR) => {}
|
||||
Err(e) => return Err(std::io::Error::from_raw_os_error(e as i32))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TermReader {
|
||||
buffer: BufReader<TermBuffer>
|
||||
}
|
||||
|
||||
impl TermReader {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
buffer: BufReader::new(TermBuffer::new(1))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, timeout: PollTimeout) -> ShResult<bool> {
|
||||
if self.buffer.buffer().len() > 0 {
|
||||
return Ok(true)
|
||||
}
|
||||
|
||||
let mut fds = [poll::PollFd::new(self.as_fd(),PollFlags::POLLIN)];
|
||||
let r = poll::poll(&mut fds, timeout);
|
||||
match r {
|
||||
Ok(n) => Ok(n != 0),
|
||||
Err(Errno::EINTR) => Ok(false),
|
||||
Err(e) => Err(e.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_byte(&mut self) -> std::io::Result<u8> {
|
||||
let mut buf = [0u8];
|
||||
self.buffer.read_exact(&mut buf)?;
|
||||
Ok(buf[0])
|
||||
}
|
||||
|
||||
pub fn peek_byte(&mut self) -> std::io::Result<u8> {
|
||||
let buf = self.buffer.fill_buf()?;
|
||||
if buf.is_empty() {
|
||||
Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "EOF"))
|
||||
} else {
|
||||
Ok(buf[0])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume_byte(&mut self) {
|
||||
self.buffer.consume(1);
|
||||
}
|
||||
}
|
||||
|
||||
impl AsFd for TermReader {
|
||||
fn as_fd(&self) -> BorrowedFd<'_> {
|
||||
let fd = self.buffer.get_ref().tty;
|
||||
unsafe { BorrowedFd::borrow_raw(fd) }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Layout {
|
||||
pub w_calc: Box<dyn WidthCalculator>,
|
||||
pub prompt_end: Pos,
|
||||
pub cursor: Pos,
|
||||
pub end: Pos
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn new() -> Self {
|
||||
let w_calc = width_calculator();
|
||||
Self {
|
||||
w_calc,
|
||||
prompt_end: Pos::default(),
|
||||
cursor: Pos::default(),
|
||||
end: Pos::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LineWriter {
|
||||
out: RawFd,
|
||||
t_cols: Col, // terminal width
|
||||
buffer: String,
|
||||
w_calc: Box<dyn WidthCalculator>,
|
||||
tab_stop: u16,
|
||||
}
|
||||
|
||||
impl LineWriter {
|
||||
pub fn new(out: RawFd) -> Self {
|
||||
let w_calc = width_calculator();
|
||||
let (t_cols,_) = get_win_size(out);
|
||||
Self {
|
||||
out,
|
||||
t_cols,
|
||||
buffer: String::new(),
|
||||
w_calc,
|
||||
tab_stop: 8 // TODO: add a way to configure this
|
||||
}
|
||||
}
|
||||
pub fn flush_write(&mut self, buf: &str) -> ShResult<()> {
|
||||
write_all(self.out, buf)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn clear_rows(&mut self, layout: &Layout) {
|
||||
let rows_to_clear = layout.end.row;
|
||||
let cursor_row = layout.cursor.row;
|
||||
|
||||
let cursor_motion = rows_to_clear.saturating_sub(cursor_row);
|
||||
if cursor_motion > 0 {
|
||||
write!(self.buffer, "\x1b[{cursor_motion}B").unwrap()
|
||||
}
|
||||
|
||||
for _ in 0..rows_to_clear {
|
||||
self.buffer.push_str("\x1b[K\x1b[A");
|
||||
}
|
||||
self.buffer.push_str("\x1b[K");
|
||||
}
|
||||
pub fn move_cursor(&mut self, old: Pos, new: Pos) -> ShResult<()> {
|
||||
self.buffer.clear();
|
||||
let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal 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)?
|
||||
}
|
||||
}
|
||||
|
||||
// Try parse valid UTF-8 from collected bytes
|
||||
if let Ok(s) = str::from_utf8(&collected) {
|
||||
return KeyEvent::new(s, ModKeys::empty());
|
||||
}
|
||||
|
||||
// If it's not valid UTF-8 yet, loop to collect more bytes
|
||||
if collected.len() >= 4 {
|
||||
// UTF-8 max char length is 4; if it's still invalid, give up
|
||||
break;
|
||||
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)?
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Equal => { /* Do nothing */ }
|
||||
}
|
||||
|
||||
KeyEvent(KeyCode::Null, ModKeys::empty())
|
||||
match new.col.cmp(&old.col) {
|
||||
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)?
|
||||
}
|
||||
}
|
||||
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)?
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Equal => { /* Do nothing */ }
|
||||
}
|
||||
write_all(self.out, self.buffer.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_esc_seq(&self, buf: &mut [u8]) -> Option<KeyEvent> {
|
||||
let mut collected = vec![0x1b];
|
||||
pub fn redraw(
|
||||
&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();
|
||||
|
||||
// Peek next byte
|
||||
let _ = self.peek_byte(&mut buf[..1]);
|
||||
let b1 = buf[0];
|
||||
collected.push(b1);
|
||||
self.clear_rows(old_layout);
|
||||
|
||||
match b1 {
|
||||
b'[' => {
|
||||
// Next byte(s) determine the sequence
|
||||
let _ = self.peek_byte(&mut buf[..1]);
|
||||
let b2 = buf[0];
|
||||
collected.push(b2);
|
||||
let end = new_layout.end;
|
||||
let cursor = new_layout.cursor;
|
||||
|
||||
match b2 {
|
||||
b'A' => Some(KeyEvent(KeyCode::Up, ModKeys::empty())),
|
||||
b'B' => Some(KeyEvent(KeyCode::Down, ModKeys::empty())),
|
||||
b'C' => Some(KeyEvent(KeyCode::Right, ModKeys::empty())),
|
||||
b'D' => Some(KeyEvent(KeyCode::Left, ModKeys::empty())),
|
||||
b'1'..=b'9' => {
|
||||
// Might be Delete/Home/etc
|
||||
let mut digits = vec![b2];
|
||||
self.buffer.push_str(prompt);
|
||||
self.buffer.push_str(line.as_str());
|
||||
|
||||
// Keep reading until we hit `~` or `;` (modifiers)
|
||||
loop {
|
||||
let _ = self.peek_byte(&mut buf[..1]);
|
||||
let b = buf[0];
|
||||
collected.push(b);
|
||||
if end.col == 0
|
||||
&& end.row > 0
|
||||
{
|
||||
// The line has wrapped. We need to use our own line break.
|
||||
self.buffer.push('\n')
|
||||
}
|
||||
|
||||
if b == b'~' {
|
||||
break;
|
||||
} else if b == b';' {
|
||||
// modifier-aware sequence, like `ESC [ 1 ; 5 ~`
|
||||
// You may want to parse the full thing
|
||||
break;
|
||||
} else if !b.is_ascii_digit() {
|
||||
break;
|
||||
} else {
|
||||
digits.push(b);
|
||||
}
|
||||
}
|
||||
let cursor_row_offset = end.row - cursor.row;
|
||||
|
||||
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
|
||||
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)?
|
||||
}
|
||||
|
||||
// Function keys
|
||||
[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,
|
||||
};
|
||||
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)?
|
||||
}
|
||||
|
||||
Some(KeyEvent(key, ModKeys::empty()))
|
||||
}
|
||||
_ => Some(KeyEvent(KeyCode::Esc, ModKeys::empty())),
|
||||
}
|
||||
}
|
||||
b'O' => {
|
||||
let _ = self.peek_byte(&mut buf[..1]);
|
||||
let b2 = buf[0];
|
||||
collected.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,
|
||||
};
|
||||
|
||||
Some(KeyEvent(key, ModKeys::empty()))
|
||||
}
|
||||
_ => Some(KeyEvent(KeyCode::Esc, ModKeys::empty())),
|
||||
}
|
||||
write_all(self.out, self.buffer.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cursor_pos(&mut self) -> (usize, usize) {
|
||||
self.write_unrecorded("\x1b[6n");
|
||||
let mut buf = [0u8;32];
|
||||
let n = self.read_byte(&mut buf);
|
||||
|
||||
|
||||
let response = std::str::from_utf8(&buf[..n]).unwrap_or("");
|
||||
let mut row = 0;
|
||||
let mut col = 0;
|
||||
if let Some(caps) = response.strip_prefix("\x1b[").and_then(|s| s.strip_suffix("R")) {
|
||||
let mut parts = caps.split(';');
|
||||
if let (Some(rowstr), Some(colstr)) = (parts.next(), parts.next()) {
|
||||
row = rowstr.parse().unwrap_or(1);
|
||||
col = colstr.parse().unwrap_or(1);
|
||||
pub fn calc_pos(&self, s: &str, orig: Pos) -> Pos {
|
||||
let mut pos = orig;
|
||||
let mut esc_seq = 0;
|
||||
for c in s.graphemes(true) {
|
||||
if c == "\n" {
|
||||
pos.row += 1;
|
||||
pos.col = 0;
|
||||
}
|
||||
let c_width = if c == "\t" {
|
||||
self.tab_stop - (pos.col % self.tab_stop)
|
||||
} else {
|
||||
width(c, &mut esc_seq)
|
||||
};
|
||||
pos.col += c_width;
|
||||
if pos.col > self.t_cols {
|
||||
pos.row += 1;
|
||||
pos.col = c_width;
|
||||
}
|
||||
}
|
||||
(row,col)
|
||||
}
|
||||
}
|
||||
if pos.col > self.t_cols {
|
||||
pos.row += 1;
|
||||
pos.col = 0;
|
||||
}
|
||||
|
||||
impl Default for Terminal {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
pos
|
||||
}
|
||||
|
||||
pub fn update_t_cols(&mut self) {
|
||||
let (t_cols,_) = get_win_size(self.out);
|
||||
self.t_cols = t_cols;
|
||||
}
|
||||
|
||||
pub fn move_cursor_at_leftmost(&mut self, rdr: &mut TermReader) -> ShResult<()> {
|
||||
if rdr.poll(PollTimeout::ZERO)? {
|
||||
// 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")?;
|
||||
|
||||
// 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
|
||||
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")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user