1#![forbid(unsafe_code)]
2
3use std::io::{self, Write};
44
45use crate::terminal_capabilities::TerminalCapabilities;
46
47const DEC_SAVE: &[u8] = b"\x1b7";
51
52const DEC_RESTORE: &[u8] = b"\x1b8";
56
57const ANSI_SAVE: &[u8] = b"\x1b[s";
61
62const ANSI_RESTORE: &[u8] = b"\x1b[u";
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
73pub enum CursorSaveStrategy {
74 #[default]
79 Dec,
80
81 Ansi,
86
87 Emulated,
92}
93
94impl CursorSaveStrategy {
95 #[must_use]
99 pub fn detect(caps: &TerminalCapabilities) -> Self {
100 if caps.in_screen {
102 return Self::Ansi;
103 }
104
105 Self::Dec
108 }
109
110 #[must_use]
114 pub const fn save_sequence(&self) -> Option<&'static [u8]> {
115 match self {
116 Self::Dec => Some(DEC_SAVE),
117 Self::Ansi => Some(ANSI_SAVE),
118 Self::Emulated => None,
119 }
120 }
121
122 #[must_use]
126 pub const fn restore_sequence(&self) -> Option<&'static [u8]> {
127 match self {
128 Self::Dec => Some(DEC_RESTORE),
129 Self::Ansi => Some(ANSI_RESTORE),
130 Self::Emulated => None,
131 }
132 }
133}
134
135#[derive(Debug, Clone)]
147pub struct CursorManager {
148 strategy: CursorSaveStrategy,
149 saved_position: Option<(u16, u16)>,
151}
152
153impl CursorManager {
154 #[must_use]
156 pub const fn new(strategy: CursorSaveStrategy) -> Self {
157 Self {
158 strategy,
159 saved_position: None,
160 }
161 }
162
163 #[must_use]
165 pub fn detect(caps: &TerminalCapabilities) -> Self {
166 Self::new(CursorSaveStrategy::detect(caps))
167 }
168
169 #[must_use]
171 pub const fn strategy(&self) -> CursorSaveStrategy {
172 self.strategy
173 }
174
175 pub fn save<W: Write>(&mut self, writer: &mut W, current_pos: (u16, u16)) -> io::Result<()> {
187 match self.strategy {
188 CursorSaveStrategy::Dec => writer.write_all(DEC_SAVE),
189 CursorSaveStrategy::Ansi => writer.write_all(ANSI_SAVE),
190 CursorSaveStrategy::Emulated => {
191 self.saved_position = Some(current_pos);
192 Ok(())
193 }
194 }
195 }
196
197 pub fn restore<W: Write>(&self, writer: &mut W) -> io::Result<()> {
204 match self.strategy {
205 CursorSaveStrategy::Dec => writer.write_all(DEC_RESTORE),
206 CursorSaveStrategy::Ansi => writer.write_all(ANSI_RESTORE),
207 CursorSaveStrategy::Emulated => {
208 if let Some((col, row)) = self.saved_position {
209 write!(writer, "\x1b[{};{}H", row + 1, col + 1)
211 } else {
212 Ok(())
213 }
214 }
215 }
216 }
217
218 pub fn clear(&mut self) {
222 self.saved_position = None;
223 }
224
225 #[must_use]
229 pub const fn saved_position(&self) -> Option<(u16, u16)> {
230 self.saved_position
231 }
232}
233
234impl Default for CursorManager {
235 fn default() -> Self {
236 Self::new(CursorSaveStrategy::default())
237 }
238}
239
240pub fn move_to<W: Write>(writer: &mut W, col: u16, row: u16) -> io::Result<()> {
254 write!(writer, "\x1b[{};{}H", row + 1, col + 1)
256}
257
258pub fn hide<W: Write>(writer: &mut W) -> io::Result<()> {
262 writer.write_all(b"\x1b[?25l")
263}
264
265pub fn show<W: Write>(writer: &mut W) -> io::Result<()> {
269 writer.write_all(b"\x1b[?25h")
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn dec_save_restore_sequences() {
278 let strategy = CursorSaveStrategy::Dec;
279 assert_eq!(strategy.save_sequence(), Some(b"\x1b7".as_slice()));
280 assert_eq!(strategy.restore_sequence(), Some(b"\x1b8".as_slice()));
281 }
282
283 #[test]
284 fn ansi_save_restore_sequences() {
285 let strategy = CursorSaveStrategy::Ansi;
286 assert_eq!(strategy.save_sequence(), Some(b"\x1b[s".as_slice()));
287 assert_eq!(strategy.restore_sequence(), Some(b"\x1b[u".as_slice()));
288 }
289
290 #[test]
291 fn emulated_has_no_sequences() {
292 let strategy = CursorSaveStrategy::Emulated;
293 assert_eq!(strategy.save_sequence(), None);
294 assert_eq!(strategy.restore_sequence(), None);
295 }
296
297 #[test]
298 fn detect_uses_dec_for_normal_terminal() {
299 let caps = TerminalCapabilities::basic();
300 let strategy = CursorSaveStrategy::detect(&caps);
301 assert_eq!(strategy, CursorSaveStrategy::Dec);
302 }
303
304 #[test]
305 fn detect_uses_ansi_for_screen() {
306 let mut caps = TerminalCapabilities::basic();
307 caps.in_screen = true;
308 let strategy = CursorSaveStrategy::detect(&caps);
309 assert_eq!(strategy, CursorSaveStrategy::Ansi);
310 }
311
312 #[test]
313 fn detect_uses_dec_for_tmux() {
314 let mut caps = TerminalCapabilities::basic();
315 caps.in_tmux = true;
316 let strategy = CursorSaveStrategy::detect(&caps);
317 assert_eq!(strategy, CursorSaveStrategy::Dec);
318 }
319
320 #[test]
321 fn cursor_manager_dec_save() {
322 let mut manager = CursorManager::new(CursorSaveStrategy::Dec);
323 let mut output = Vec::new();
324
325 manager.save(&mut output, (10, 5)).unwrap();
326 assert_eq!(output, b"\x1b7");
327 }
328
329 #[test]
330 fn cursor_manager_dec_restore() {
331 let manager = CursorManager::new(CursorSaveStrategy::Dec);
332 let mut output = Vec::new();
333
334 manager.restore(&mut output).unwrap();
335 assert_eq!(output, b"\x1b8");
336 }
337
338 #[test]
339 fn cursor_manager_ansi_save_restore() {
340 let mut manager = CursorManager::new(CursorSaveStrategy::Ansi);
341 let mut output = Vec::new();
342
343 manager.save(&mut output, (0, 0)).unwrap();
344 assert_eq!(output, b"\x1b[s");
345
346 output.clear();
347 manager.restore(&mut output).unwrap();
348 assert_eq!(output, b"\x1b[u");
349 }
350
351 #[test]
352 fn cursor_manager_emulated_save_restore() {
353 let mut manager = CursorManager::new(CursorSaveStrategy::Emulated);
354 let mut output = Vec::new();
355
356 manager.save(&mut output, (10, 5)).unwrap();
358 assert!(output.is_empty()); assert_eq!(manager.saved_position(), Some((10, 5)));
360
361 manager.restore(&mut output).unwrap();
363 assert_eq!(output, b"\x1b[6;11H"); }
365
366 #[test]
367 fn cursor_manager_emulated_restore_without_save() {
368 let manager = CursorManager::new(CursorSaveStrategy::Emulated);
369 let mut output = Vec::new();
370
371 manager.restore(&mut output).unwrap();
373 assert!(output.is_empty());
374 }
375
376 #[test]
377 fn cursor_manager_clear() {
378 let mut manager = CursorManager::new(CursorSaveStrategy::Emulated);
379 let mut output = Vec::new();
380
381 manager.save(&mut output, (5, 10)).unwrap();
382 assert_eq!(manager.saved_position(), Some((5, 10)));
383
384 manager.clear();
385 assert_eq!(manager.saved_position(), None);
386 }
387
388 #[test]
389 fn cursor_manager_default_uses_dec() {
390 let manager = CursorManager::default();
391 assert_eq!(manager.strategy(), CursorSaveStrategy::Dec);
392 }
393
394 #[test]
395 fn move_to_outputs_cup() {
396 let mut output = Vec::new();
397 move_to(&mut output, 0, 0).unwrap();
398 assert_eq!(output, b"\x1b[1;1H");
399
400 output.clear();
401 move_to(&mut output, 79, 23).unwrap();
402 assert_eq!(output, b"\x1b[24;80H");
403 }
404
405 #[test]
406 fn hide_and_show_cursor() {
407 let mut output = Vec::new();
408
409 hide(&mut output).unwrap();
410 assert_eq!(output, b"\x1b[?25l");
411
412 output.clear();
413 show(&mut output).unwrap();
414 assert_eq!(output, b"\x1b[?25h");
415 }
416
417 #[test]
418 fn emulated_save_overwrites_previous_position() {
419 let mut manager = CursorManager::new(CursorSaveStrategy::Emulated);
420 let mut output = Vec::new();
421
422 manager.save(&mut output, (1, 2)).unwrap();
423 assert_eq!(manager.saved_position(), Some((1, 2)));
424
425 manager.save(&mut output, (30, 40)).unwrap();
426 assert_eq!(manager.saved_position(), Some((30, 40)));
427
428 manager.restore(&mut output).unwrap();
429 assert_eq!(output, b"\x1b[41;31H");
430 }
431
432 #[test]
433 fn cursor_save_strategy_default_is_dec() {
434 let strategy = CursorSaveStrategy::default();
435 assert_eq!(strategy, CursorSaveStrategy::Dec);
436 }
437
438 #[test]
439 fn cursor_manager_clone_preserves_saved_position() {
440 let mut manager = CursorManager::new(CursorSaveStrategy::Emulated);
441 let mut output = Vec::new();
442 manager.save(&mut output, (7, 13)).unwrap();
443
444 let cloned = manager.clone();
445 assert_eq!(cloned.saved_position(), Some((7, 13)));
446 assert_eq!(cloned.strategy(), CursorSaveStrategy::Emulated);
447 }
448}