#![forbid(unsafe_code)]
use std::io::{self, Write};
use crate::terminal_capabilities::TerminalCapabilities;
const DEC_SAVE: &[u8] = b"\x1b7";
const DEC_RESTORE: &[u8] = b"\x1b8";
const ANSI_SAVE: &[u8] = b"\x1b[s";
const ANSI_RESTORE: &[u8] = b"\x1b[u";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CursorSaveStrategy {
#[default]
Dec,
Ansi,
Emulated,
}
impl CursorSaveStrategy {
#[must_use]
pub fn detect(caps: &TerminalCapabilities) -> Self {
if caps.in_screen {
return Self::Ansi;
}
Self::Dec
}
#[must_use]
pub const fn save_sequence(&self) -> Option<&'static [u8]> {
match self {
Self::Dec => Some(DEC_SAVE),
Self::Ansi => Some(ANSI_SAVE),
Self::Emulated => None,
}
}
#[must_use]
pub const fn restore_sequence(&self) -> Option<&'static [u8]> {
match self {
Self::Dec => Some(DEC_RESTORE),
Self::Ansi => Some(ANSI_RESTORE),
Self::Emulated => None,
}
}
}
#[derive(Debug, Clone)]
pub struct CursorManager {
strategy: CursorSaveStrategy,
saved_position: Option<(u16, u16)>,
}
impl CursorManager {
#[must_use]
pub const fn new(strategy: CursorSaveStrategy) -> Self {
Self {
strategy,
saved_position: None,
}
}
#[must_use]
pub fn detect(caps: &TerminalCapabilities) -> Self {
Self::new(CursorSaveStrategy::detect(caps))
}
#[must_use]
pub const fn strategy(&self) -> CursorSaveStrategy {
self.strategy
}
pub fn save<W: Write>(&mut self, writer: &mut W, current_pos: (u16, u16)) -> io::Result<()> {
match self.strategy {
CursorSaveStrategy::Dec => writer.write_all(DEC_SAVE),
CursorSaveStrategy::Ansi => writer.write_all(ANSI_SAVE),
CursorSaveStrategy::Emulated => {
self.saved_position = Some(current_pos);
Ok(())
}
}
}
pub fn restore<W: Write>(&self, writer: &mut W) -> io::Result<()> {
match self.strategy {
CursorSaveStrategy::Dec => writer.write_all(DEC_RESTORE),
CursorSaveStrategy::Ansi => writer.write_all(ANSI_RESTORE),
CursorSaveStrategy::Emulated => {
if let Some((col, row)) = self.saved_position {
write!(writer, "\x1b[{};{}H", (row as u32) + 1, (col as u32) + 1)
} else {
Ok(())
}
}
}
}
pub fn clear(&mut self) {
self.saved_position = None;
}
#[must_use]
pub const fn saved_position(&self) -> Option<(u16, u16)> {
self.saved_position
}
}
impl Default for CursorManager {
fn default() -> Self {
Self::new(CursorSaveStrategy::default())
}
}
pub fn move_to<W: Write>(writer: &mut W, col: u16, row: u16) -> io::Result<()> {
write!(writer, "\x1b[{};{}H", (row as u32) + 1, (col as u32) + 1)
}
pub fn hide<W: Write>(writer: &mut W) -> io::Result<()> {
writer.write_all(b"\x1b[?25l")
}
pub fn show<W: Write>(writer: &mut W) -> io::Result<()> {
writer.write_all(b"\x1b[?25h")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dec_save_restore_sequences() {
let strategy = CursorSaveStrategy::Dec;
assert_eq!(strategy.save_sequence(), Some(b"\x1b7".as_slice()));
assert_eq!(strategy.restore_sequence(), Some(b"\x1b8".as_slice()));
}
#[test]
fn ansi_save_restore_sequences() {
let strategy = CursorSaveStrategy::Ansi;
assert_eq!(strategy.save_sequence(), Some(b"\x1b[s".as_slice()));
assert_eq!(strategy.restore_sequence(), Some(b"\x1b[u".as_slice()));
}
#[test]
fn emulated_has_no_sequences() {
let strategy = CursorSaveStrategy::Emulated;
assert_eq!(strategy.save_sequence(), None);
assert_eq!(strategy.restore_sequence(), None);
}
#[test]
fn detect_uses_dec_for_normal_terminal() {
let caps = TerminalCapabilities::basic();
let strategy = CursorSaveStrategy::detect(&caps);
assert_eq!(strategy, CursorSaveStrategy::Dec);
}
#[test]
fn detect_uses_ansi_for_screen() {
let mut caps = TerminalCapabilities::basic();
caps.in_screen = true;
let strategy = CursorSaveStrategy::detect(&caps);
assert_eq!(strategy, CursorSaveStrategy::Ansi);
}
#[test]
fn detect_uses_dec_for_tmux() {
let mut caps = TerminalCapabilities::basic();
caps.in_tmux = true;
let strategy = CursorSaveStrategy::detect(&caps);
assert_eq!(strategy, CursorSaveStrategy::Dec);
}
#[test]
fn cursor_manager_dec_save() {
let mut manager = CursorManager::new(CursorSaveStrategy::Dec);
let mut output = Vec::new();
manager.save(&mut output, (10, 5)).unwrap();
assert_eq!(output, b"\x1b7");
}
#[test]
fn cursor_manager_dec_restore() {
let manager = CursorManager::new(CursorSaveStrategy::Dec);
let mut output = Vec::new();
manager.restore(&mut output).unwrap();
assert_eq!(output, b"\x1b8");
}
#[test]
fn cursor_manager_ansi_save_restore() {
let mut manager = CursorManager::new(CursorSaveStrategy::Ansi);
let mut output = Vec::new();
manager.save(&mut output, (0, 0)).unwrap();
assert_eq!(output, b"\x1b[s");
output.clear();
manager.restore(&mut output).unwrap();
assert_eq!(output, b"\x1b[u");
}
#[test]
fn cursor_manager_emulated_save_restore() {
let mut manager = CursorManager::new(CursorSaveStrategy::Emulated);
let mut output = Vec::new();
manager.save(&mut output, (10, 5)).unwrap();
assert!(output.is_empty()); assert_eq!(manager.saved_position(), Some((10, 5)));
manager.restore(&mut output).unwrap();
assert_eq!(output, b"\x1b[6;11H"); }
#[test]
fn cursor_manager_emulated_restore_without_save() {
let manager = CursorManager::new(CursorSaveStrategy::Emulated);
let mut output = Vec::new();
manager.restore(&mut output).unwrap();
assert!(output.is_empty());
}
#[test]
fn cursor_manager_clear() {
let mut manager = CursorManager::new(CursorSaveStrategy::Emulated);
let mut output = Vec::new();
manager.save(&mut output, (5, 10)).unwrap();
assert_eq!(manager.saved_position(), Some((5, 10)));
manager.clear();
assert_eq!(manager.saved_position(), None);
}
#[test]
fn cursor_manager_default_uses_dec() {
let manager = CursorManager::default();
assert_eq!(manager.strategy(), CursorSaveStrategy::Dec);
}
#[test]
fn move_to_outputs_cup() {
let mut output = Vec::new();
move_to(&mut output, 0, 0).unwrap();
assert_eq!(output, b"\x1b[1;1H");
output.clear();
move_to(&mut output, 79, 23).unwrap();
assert_eq!(output, b"\x1b[24;80H");
}
#[test]
fn hide_and_show_cursor() {
let mut output = Vec::new();
hide(&mut output).unwrap();
assert_eq!(output, b"\x1b[?25l");
output.clear();
show(&mut output).unwrap();
assert_eq!(output, b"\x1b[?25h");
}
#[test]
fn emulated_save_overwrites_previous_position() {
let mut manager = CursorManager::new(CursorSaveStrategy::Emulated);
let mut output = Vec::new();
manager.save(&mut output, (1, 2)).unwrap();
assert_eq!(manager.saved_position(), Some((1, 2)));
manager.save(&mut output, (30, 40)).unwrap();
assert_eq!(manager.saved_position(), Some((30, 40)));
manager.restore(&mut output).unwrap();
assert_eq!(output, b"\x1b[41;31H");
}
#[test]
fn cursor_save_strategy_default_is_dec() {
let strategy = CursorSaveStrategy::default();
assert_eq!(strategy, CursorSaveStrategy::Dec);
}
#[test]
fn cursor_manager_clone_preserves_saved_position() {
let mut manager = CursorManager::new(CursorSaveStrategy::Emulated);
let mut output = Vec::new();
manager.save(&mut output, (7, 13)).unwrap();
let cloned = manager.clone();
assert_eq!(cloned.saved_position(), Some((7, 13)));
assert_eq!(cloned.strategy(), CursorSaveStrategy::Emulated);
}
}