diff --git a/src/common_term.rs b/src/common_term.rs index 020660a5..00bb3da8 100644 --- a/src/common_term.rs +++ b/src/common_term.rs @@ -38,6 +38,43 @@ pub fn move_cursor_to(out: &Term, x: usize, y: usize) -> io::Result<()> { out.write_str(&format!("\x1B[{};{}H", y + 1, x + 1)) } +#[cfg(unix)] +/// Return the current cursor's position as a tuple `(n, m)`, +/// where `n` is the row and `m` the column of the cursor (both 1-based). +pub fn get_cursor_position(mut out: &Term) -> io::Result<(u16, u16)> { + // Send the code "ESC[6n" to the terminal: asking for the current cursor position. + out.write_str("\x1b[6n")?; + // We expect a response of the form "ESC[n;mR", where n and m are the row and column of the cursor. + // n and m are at most 65536, so 4+2*5 bytes should suffice for these purposes. + // TODO: this blocks for user input! + let mut buf = [0u8; 4 + 2 * 5]; + let num_read = io::Read::read(&mut out, &mut buf)?; + out.clear_chars(num_read)?; + // FIXME: re-use ANSI code parser instead of rolling my own. + match &buf[..] { + [b'\x1B', b'[', middle @ .., b'R'] => { + // A valid terminal response means `middle` is valid UTF-8. + // Use string methods to simplify the parsing of input. + let middle = match std::str::from_utf8(middle) { + Ok(m) => m, + Err(_) => return Err(io::Error::new(io::ErrorKind::Other, format!("invalid terminal response: middle part of the output {:?} must be valid UTF-8", buf))), + }; + let parts = middle.splitn(2, ';').collect::>(); + let (nstr, mstr) = + match &parts[..] { + [a, b] => (a, b), + _ => return Err(io::Error::new(io::ErrorKind::Other, format!("invalid terminal response: middle part of the output should be of the form n;m, got {}", middle))), + }; + let (n, m) = (nstr.parse::().unwrap(), mstr.parse::().unwrap()); + Ok((n, m)) + } + _ => Err(io::Error::new( + io::ErrorKind::Other, + "invalid terminal response: should be of the form ESC[n;mR", + )), + } +} + pub fn clear_chars(out: &Term, n: usize) -> io::Result<()> { if n > 0 { out.write_str(&format!("\x1b[{}D\x1b[0K", n)) diff --git a/src/term.rs b/src/term.rs index 350a7894..3b95314e 100644 --- a/src/term.rs +++ b/src/term.rs @@ -446,10 +446,23 @@ impl Term { clear_line(self) } - /// Clear the last `n` lines before the current line. + /// Clear the last `n` lines before the current line, if possible. /// /// Position the cursor at the beginning of the first line that was cleared. + /// On UNIX, error when `n` is larger than the number of lines before the current cursor. + // FIXME: find good behaviour on windows and document it here. pub fn clear_last_lines(&self, n: usize) -> io::Result<()> { + #[cfg(unix)] + { + let (current_row, _) = get_cursor_position(self)?; + if usize::from(current_row) < n { + // We cannot move up n lines, only current_row ones. + return Err(io::Error::new( + io::ErrorKind::Other, + format!("can only move up {} lines, not {}", current_row, n), + )); + } + } self.move_cursor_up(n)?; for _ in 0..n { self.clear_line()?;