use std::{ collections::VecDeque, env, fmt::{Debug, Write}, io::{BufRead, BufReader, Read}, os::fd::{AsFd, BorrowedFd, RawFd}, time::Instant, }; use nix::{ errno::Errno, libc::{self}, poll::{self, PollFlags, PollTimeout}, unistd::isatty, }; use unicode_segmentation::UnicodeSegmentation; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use vte::{Parser, Perform}; pub use crate::libsh::guards::{RawModeGuard, raw_mode}; use crate::state::{read_meta, write_meta}; use crate::{ libsh::error::{ShErr, ShErrKind, ShResult}, readline::keys::{KeyCode, ModKeys}, state::read_shopts, }; use super::keys::KeyEvent; pub type Row = u16; pub type Col = u16; #[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct Pos { pub col: Col, pub row: Row, } // 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, rows) } _ => (80, 24), } } } fn enumerate_lines(s: &str, left_pad: usize, show_numbers: bool) -> String { let total_lines = s.lines().count(); let max_num_len = total_lines.to_string().len(); s.lines() .enumerate() .fold(String::new(), |mut acc, (i, ln)| { if i == 0 { acc.push_str(ln); acc.push('\n'); } else { let num = (i + 1).to_string(); let num_pad = max_num_len - num.len(); // " 2 | " — num + padding + " | " let prefix_len = max_num_len + 3; // "N | " let trail_pad = left_pad.saturating_sub(prefix_len); let prefix = if show_numbers { format!("\x1b[0m\x1b[90m{}{num} |\x1b[0m ", " ".repeat(num_pad)) } else { " ".repeat(prefix_len + 1).to_string() }; if i == total_lines - 1 { write!(acc, "{prefix}{}{ln}", " ".repeat(trail_pad)).unwrap(); } else { writeln!(acc, "{prefix}{}{ln}", " ".repeat(trail_pad)).unwrap(); } } acc }) } 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(()) } /// Check if a string ends with a newline, ignoring any trailing ANSI escape /// sequences. fn ends_with_newline(s: &str) -> bool { let bytes = s.as_bytes(); let mut i = bytes.len(); while i > 0 { // ANSI CSI sequences end with an alphabetic byte (e.g. \x1b[0m) if bytes[i - 1].is_ascii_alphabetic() { let term = i - 1; let mut j = term; // Walk back past parameter bytes (digits and ';') while j > 0 && (bytes[j - 1].is_ascii_digit() || bytes[j - 1] == b';') { j -= 1; } // Check for CSI introducer \x1b[ if j >= 2 && bytes[j - 1] == b'[' && bytes[j - 2] == 0x1b { i = j - 2; continue; } } break; } i > 0 && bytes[i - 1] == b'\n' } pub fn calc_str_width(s: &str) -> u16 { let mut esc_seq = 0; s.graphemes(true).map(|g| width(g, &mut esc_seq)).sum() } // 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 { // two-character sequence *esc_seq = 0; } 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; } 0 } else if s == "\x1b" { *esc_seq = 1; 0 } else if s == "\n" { 0 } else { w_calc.width(s) as u16 } } pub fn width_calculator() -> Box { 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), } } fn read_digits_until(rdr: &mut TermReader, sep: char) -> ShResult> { 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; } c if c == sep => break, _ => return Ok(None), } } Ok(Some(num)) } 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; } pub trait KeyReader { fn read_key(&mut self) -> Result, ShErr>; } pub trait LineWriter { fn clear_rows(&mut self, layout: &Layout) -> ShResult<()>; fn redraw(&mut self, prompt: &str, line: &str, new_layout: &Layout) -> ShResult<()>; fn flush_write(&mut self, buf: &str) -> ShResult<()>; fn send_bell(&mut self) -> ShResult<()>; } #[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)); Self { tty } } } impl Read for TermBuffer { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { assert!(isatty(self.tty).is_ok_and(|r| r)); let result = nix::unistd::read(self.tty, buf); match result { Ok(n) => Ok(n), Err(Errno::EINTR) => Err(Errno::EINTR.into()), Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)), } } } // ============================================================================ // PollReader - non-blocking key reader using vte parser // ============================================================================ struct KeyCollector { events: VecDeque, } impl KeyCollector { fn new() -> Self { Self { events: VecDeque::new(), } } fn push(&mut self, event: KeyEvent) { self.events.push_back(event); } fn pop(&mut self) -> Option { self.events.pop_front() } /// Parse modifier bits from CSI parameter (e.g., 1;5A means Ctrl+Up) fn parse_modifiers(param: u16) -> ModKeys { // CSI modifiers: param = 1 + (shift) + (alt*2) + (ctrl*4) + (meta*8) let bits = param.saturating_sub(1); let mut mods = ModKeys::empty(); if bits & 1 != 0 { mods |= ModKeys::SHIFT; } if bits & 2 != 0 { mods |= ModKeys::ALT; } if bits & 4 != 0 { mods |= ModKeys::CTRL; } mods } } impl Default for KeyCollector { fn default() -> Self { Self::new() } } impl Perform for KeyCollector { fn print(&mut self, c: char) { // vte routes 0x7f (DEL) to print instead of execute if c == '\x7f' { self.push(KeyEvent(KeyCode::Backspace, ModKeys::empty())); } else { self.push(KeyEvent(KeyCode::Char(c), ModKeys::empty())); } } fn execute(&mut self, byte: u8) { let event = match byte { 0x00 => KeyEvent(KeyCode::Char(' '), ModKeys::CTRL), // Ctrl+Space / Ctrl+@ 0x09 => KeyEvent(KeyCode::Tab, ModKeys::empty()), // Tab (Ctrl+I) 0x0a => KeyEvent(KeyCode::Char('j'), ModKeys::CTRL), // Ctrl+J (linefeed) 0x0d => KeyEvent(KeyCode::Enter, ModKeys::empty()), // Carriage return (Ctrl+M) 0x1b => KeyEvent(KeyCode::Esc, ModKeys::empty()), 0x7f => KeyEvent(KeyCode::Backspace, ModKeys::empty()), 0x01..=0x1a => { // Ctrl+A through Ctrl+Z (excluding special cases above) let c = (b'A' + byte - 1) as char; KeyEvent(KeyCode::Char(c), ModKeys::CTRL) } _ => return, }; self.push(event); } fn csi_dispatch( &mut self, params: &vte::Params, intermediates: &[u8], _ignore: bool, action: char, ) { let params: Vec = params .iter() .map(|p| p.first().copied().unwrap_or(0)) .collect(); let event = match (intermediates, action) { // Arrow keys: CSI A/B/C/D or CSI 1;mod A/B/C/D ([], 'A') => { let mods = params .get(1) .map(|&m| Self::parse_modifiers(m)) .unwrap_or(ModKeys::empty()); KeyEvent(KeyCode::Up, mods) } ([], 'B') => { let mods = params .get(1) .map(|&m| Self::parse_modifiers(m)) .unwrap_or(ModKeys::empty()); KeyEvent(KeyCode::Down, mods) } ([], 'C') => { let mods = params .get(1) .map(|&m| Self::parse_modifiers(m)) .unwrap_or(ModKeys::empty()); KeyEvent(KeyCode::Right, mods) } ([], 'D') => { let mods = params .get(1) .map(|&m| Self::parse_modifiers(m)) .unwrap_or(ModKeys::empty()); KeyEvent(KeyCode::Left, mods) } // Home/End: CSI H/F or CSI 1;mod H/F ([], 'H') => { let mods = params .get(1) .map(|&m| Self::parse_modifiers(m)) .unwrap_or(ModKeys::empty()); KeyEvent(KeyCode::Home, mods) } ([], 'F') => { let mods = params .get(1) .map(|&m| Self::parse_modifiers(m)) .unwrap_or(ModKeys::empty()); KeyEvent(KeyCode::End, mods) } // Shift+Tab: CSI Z ([], 'Z') => KeyEvent(KeyCode::Tab, ModKeys::SHIFT), // Special keys with tilde: CSI num ~ or CSI num;mod ~ ([], '~') => { let key_num = params.first().copied().unwrap_or(0); let mods = params .get(1) .map(|&m| Self::parse_modifiers(m)) .unwrap_or(ModKeys::empty()); let key = match key_num { 1 | 7 => KeyCode::Home, 2 => KeyCode::Insert, 3 => KeyCode::Delete, 4 | 8 => KeyCode::End, 5 => KeyCode::PageUp, 6 => KeyCode::PageDown, 15 => KeyCode::F(5), 17 => KeyCode::F(6), 18 => KeyCode::F(7), 19 => KeyCode::F(8), 20 => KeyCode::F(9), 21 => KeyCode::F(10), 23 => KeyCode::F(11), 24 => KeyCode::F(12), 200 => KeyCode::BracketedPasteStart, 201 => KeyCode::BracketedPasteEnd, _ => return, }; KeyEvent(key, mods) } ([], 'u') => { let codepoint = params.first().copied().unwrap_or(0); let mods = params .get(1) .map(|&m| Self::parse_modifiers(m)) .unwrap_or(ModKeys::empty()); let key = match codepoint { 9 => KeyCode::Tab, 13 => KeyCode::Enter, 27 => KeyCode::Esc, 127 => KeyCode::Backspace, _ => { if let Some(ch) = char::from_u32(codepoint as u32) { KeyCode::Char(ch) } else { return; } } }; KeyEvent(key, mods) } // SGR mouse: CSI < button;x;y M/m (ignore mouse events for now) ([b'<'], 'M') | ([b'<'], 'm') => { return; } _ => return, }; self.push(event); } fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) { // SS3 sequences (ESC O P/Q/R/S for F1-F4) if intermediates == [b'O'] { let key = match byte { b'P' => KeyCode::F(1), b'Q' => KeyCode::F(2), b'R' => KeyCode::F(3), b'S' => KeyCode::F(4), _ => return, }; self.push(KeyEvent(key, ModKeys::empty())); } } } pub struct PollReader { parser: Parser, collector: KeyCollector, byte_buf: VecDeque, pub verbatim_single: bool, pub verbatim: bool, } impl PollReader { pub fn new() -> Self { Self { parser: Parser::new(), collector: KeyCollector::new(), byte_buf: VecDeque::new(), verbatim_single: false, verbatim: false, } } pub fn handle_bracket_paste(&mut self) -> Option { let end_marker = b"\x1b[201~"; let mut raw = vec![]; while let Some(byte) = self.byte_buf.pop_front() { raw.push(byte); if raw.ends_with(end_marker) { // Strip the end marker from the raw sequence raw.truncate(raw.len() - end_marker.len()); let paste = String::from_utf8_lossy(&raw).to_string(); self.verbatim = false; return Some(KeyEvent(KeyCode::Verbatim(paste.into()), ModKeys::empty())); } } self.verbatim = true; self.byte_buf.extend(raw); None } pub fn read_one_verbatim(&mut self) -> Option { if self.byte_buf.is_empty() { return None; } let bytes: Vec = self.byte_buf.drain(..).collect(); let verbatim_str = String::from_utf8_lossy(&bytes).to_string(); Some(KeyEvent(KeyCode::Verbatim(verbatim_str.into()), ModKeys::empty())) } pub fn feed_bytes(&mut self, bytes: &[u8]) { self.byte_buf.extend(bytes); } } impl Default for PollReader { fn default() -> Self { Self::new() } } impl KeyReader for PollReader { fn read_key(&mut self) -> Result, ShErr> { if self.verbatim_single { if let Some(key) = self.read_one_verbatim() { self.verbatim_single = false; return Ok(Some(key)); } return Ok(None); } if self.verbatim { if let Some(paste) = self.handle_bracket_paste() { return Ok(Some(paste)); } // If we're in verbatim mode but haven't seen the end marker yet, don't attempt to parse keys return Ok(None); } else if self.byte_buf.front() == Some(&b'\x1b') { // Escape: if it's the only byte, or the next byte isn't a valid // escape sequence prefix ([ or O), emit a standalone Escape if self.byte_buf.len() == 1 || !matches!(self.byte_buf.get(1), Some(b'[') | Some(b'O')) { self.byte_buf.pop_front(); return Ok(Some(KeyEvent(KeyCode::Esc, ModKeys::empty()))); } } while let Some(byte) = self.byte_buf.pop_front() { self.parser.advance(&mut self.collector, &[byte]); if let Some(key) = self.collector.pop() { match key { KeyEvent(KeyCode::BracketedPasteStart, _) => { if let Some(paste) = self.handle_bracket_paste() { return Ok(Some(paste)); } else { continue; } } _ => return Ok(Some(key)) } } } Ok(None) } } // ============================================================================ // TermReader - blocking key reader (original implementation) // ============================================================================ pub struct TermReader { buffer: BufReader, } impl TermReader { pub fn new(tty: RawFd) -> Self { Self { buffer: BufReader::new(TermBuffer::new(tty)), } } /// 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 { if !self.buffer.buffer().is_empty() { 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 { let mut buf = [0u8]; let _n = self.buffer.read(&mut buf)?; Ok(buf[0]) } pub fn peek_byte(&mut self) -> std::io::Result { 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); } pub fn parse_esc_seq(&mut self) -> ShResult { 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 KeyReader for TermReader { fn read_key(&mut self) -> Result, ShErr> { 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().map(Some); } // Try parse as valid UTF-8 if let Ok(s) = str::from_utf8(&collected) { return Ok(Some(KeyEvent::new(s, ModKeys::empty()))); } // UTF-8 max 4 bytes — if it’s invalid at this point, bail if collected.len() >= 4 { break; } } Ok(None) } } impl AsFd for TermReader { fn as_fd(&self) -> BorrowedFd<'_> { let fd = self.buffer.get_ref().tty; unsafe { BorrowedFd::borrow_raw(fd) } } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Layout { pub prompt_end: Pos, pub cursor: Pos, pub end: Pos, pub psr_end: Option, pub t_cols: u16, } impl Layout { pub fn new() -> Self { Self { prompt_end: Pos::default(), cursor: Pos::default(), end: Pos::default(), psr_end: None, t_cols: 0, } } pub fn from_parts(term_width: u16, prompt: &str, to_cursor: &str, to_end: &str) -> Self { let prompt_end = Self::calc_pos(term_width, prompt, Pos { col: 0, row: 0 }, 0, false); let cursor = Self::calc_pos(term_width, to_cursor, prompt_end, prompt_end.col, true); let end = Self::calc_pos(term_width, to_end, prompt_end, prompt_end.col, false); Layout { prompt_end, cursor, end, psr_end: None, t_cols: term_width, } } fn is_ctl_char(gr: &str) -> bool { !gr.is_empty() && gr.as_bytes()[0] <= 0x1F && gr != "\n" && gr != "\t" && gr != "\r" } pub fn calc_pos(term_width: u16, s: &str, orig: Pos, left_margin: u16, raw_calc: bool) -> Pos { const TAB_STOP: u16 = 8; let mut pos = orig; let mut esc_seq = 0; for c in s.graphemes(true) { if c == "\n" { pos.row += 1; pos.col = left_margin; } let c_width = if c == "\t" { TAB_STOP - (pos.col % TAB_STOP) } else if raw_calc && Self::is_ctl_char(c) { 2 } else { width(c, &mut esc_seq) }; pos.col += c_width; if pos.col > term_width { pos.row += 1; pos.col = c_width; } } if pos.col >= term_width { pos.row += 1; pos.col = 0; } pos } } impl Default for Layout { fn default() -> Self { Self::new() } } pub struct TermWriter { last_bell: Option, out: RawFd, pub t_cols: Col, // terminal width buffer: String, } impl TermWriter { pub fn new(out: RawFd) -> Self { let (t_cols, _) = get_win_size(out); Self { last_bell: None, out, t_cols, buffer: String::new(), } } pub fn get_cursor_movement(&self, old: Pos, new: Pos) -> ShResult { 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 => 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 => buffer.push_str("\x1b[A"), _ => write!(buffer, "\x1b[{shift}A").map_err(err)?, } } std::cmp::Ordering::Equal => { /* Do nothing */ } } match new.col.cmp(&old.col) { std::cmp::Ordering::Greater => { let shift = new.col - old.col; match shift { 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 => buffer.push_str("\x1b[D"), _ => write!(buffer, "\x1b[{shift}D").map_err(err)?, } } std::cmp::Ordering::Equal => { /* Do nothing */ } } 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(()) } pub fn update_t_cols(&mut self) { let (t_cols, _) = get_win_size(self.out); self.t_cols = t_cols; } /// Called before the prompt is drawn. If we are not on column 1, push a vid-inverted '%' and then a '\n\r'. /// /// Aping zsh with this but it's a nice feature. pub fn fix_cursor_column(&mut self, rdr: &mut TermReader) -> ShResult<()> { let Some((_, c)) = self.get_cursor_pos(rdr)? else { return Ok(()); }; if c != 1 { self.flush_write("\x1b[7m%\x1b[0m\n\r")?; } Ok(()) } pub fn get_cursor_pos(&mut self, rdr: &mut TermReader) -> ShResult> { // Ping the cursor's position self.flush_write("\x1b[6n")?; if !rdr.poll(PollTimeout::from(255u8))? { return Ok(None); } if rdr.next_byte()? as char != '\x1b' { return Ok(None); } if rdr.next_byte()? as char != '[' { return Ok(None); } let row = read_digits_until(rdr, ';')?; let col = read_digits_until(rdr, 'R')?; let pos = if let Some(row) = row && let Some(col) = col { Some((row as usize, col as usize)) } else { None }; Ok(pos) } 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\n")?; 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) { 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(()) } } impl LineWriter for TermWriter { fn clear_rows(&mut self, layout: &Layout) -> ShResult<()> { self.buffer.clear(); // Account for lines that may have wrapped due to terminal resize. // If a PSR was drawn, the last row extended to the old terminal width. // When the terminal shrinks, that row wraps into extra physical rows. let mut rows_to_clear = layout.end.row; if layout.psr_end.is_some() && layout.t_cols > self.t_cols && self.t_cols > 0 { let extra = (layout.t_cols.saturating_sub(1)) / self.t_cols; rows_to_clear += extra; } 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[2K\x1b[A"); } self.buffer.push_str("\x1b[2K\r"); // Clear line and return to column 0 write_all(self.out, self.buffer.as_str())?; self.buffer.clear(); Ok(()) } fn redraw(&mut self, prompt: &str, line: &str, new_layout: &Layout) -> ShResult<()> { let err = |_| { ShErr::simple( ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer", ) }; self.buffer.clear(); self.buffer.push_str("\x1b[J"); // Clear from cursor to end of screen to erase any remnants of the old line after the prompt let end = new_layout.end; let cursor = new_layout.cursor; if read_meta(|m| m.system_msg_pending()) { let mut system_msg = String::new(); while let Some(msg) = write_meta(|m| m.pop_system_message()) { writeln!(system_msg, "{msg}").map_err(err)?; } self.buffer.push_str(&system_msg); } self.buffer.push_str(prompt); let multiline = line.contains('\n'); if multiline { let prompt_end = Layout::calc_pos(self.t_cols, prompt, Pos { col: 0, row: 0 }, 0, false); let show_numbers = read_shopts(|o| o.prompt.line_numbers); let display_line = enumerate_lines(line, prompt_end.col as usize, show_numbers); self.buffer.push_str(&display_line); } else { self.buffer.push_str(line); } if end.col == 0 && end.row > 0 && !ends_with_newline(&self.buffer) { // The line has wrapped. We need to use our own line break. self.buffer.push('\n'); } let movement = self.get_cursor_movement(end, cursor)?; write!(self.buffer, "{}", &movement).map_err(err)?; write_all(self.out, self.buffer.as_str())?; Ok(()) } fn flush_write(&mut self, buf: &str) -> ShResult<()> { write_all(self.out, buf)?; Ok(()) } fn send_bell(&mut self) -> ShResult<()> { if read_shopts(|o| o.core.bell_enabled) { // we use a cooldown because I don't like having my ears assaulted by 1 million bells // whenever i finish clearing the line using backspace. let now = Instant::now(); // surprisingly, a fixed cooldown like '100' is actually more annoying than 1 million bells. // I've found this range of 50-150 to be the best balance let cooldown = rand::random_range(50..150); let should_send = match self.last_bell { None => true, Some(time) => now.duration_since(time).as_millis() > cooldown, }; if should_send { self.flush_write("\x07")?; self.last_bell = Some(now); } } Ok(()) } }