uncurses/ansi/cursor.rs
1//! Cursor addressing, movement, saving, reporting, and style sequences.
2//!
3//! ## Category
4//!
5//! This module emits cursor-related CSI, ESC, and OSC controls: absolute and
6//! relative movement, line/index movement, cursor save/restore forms, cursor
7//! style, pointer shape, and cursor-position reports.
8//!
9//! ## Coordinate conventions
10//!
11//! Public cursor-position arguments are zero-based unless the item explicitly
12//! says otherwise. Writers add one when the terminal sequence is one-based and
13//! omit default parameters when the emitted bytes support a shorter spelling.
14//!
15//! ```text
16//! row=10 col=20 → ESC [ 11 ; 21 H
17//! ──┬── ─┬─ ┬─ ┬
18//! CSI row col final
19//! ```
20//!
21//! ## Mode interaction
22//!
23//! Origin mode ([`Mode::ORIGIN`](crate::ansi::mode::Mode::ORIGIN)) changes how
24//! terminals interpret absolute row/column positions relative to scroll margins.
25//! Cursor visibility is controlled by [`Mode::CURSOR_VISIBLE`](crate::ansi::mode::Mode::CURSOR_VISIBLE), while this module emits movement and report bytes.
26
27use std::io::{self, Write};
28
29/// Move the cursor to an absolute position with CUP, `ESC [ <row+1> ; <col+1> H`.
30///
31/// `row` and `col` are zero-based API coordinates. The writer omits the column when `col == 0` and emits bare `ESC [ H` for the origin.
32pub fn write_cup<W: Write>(w: &mut W, row: u16, col: u16) -> io::Result<()> {
33 if row == 0 && col == 0 {
34 w.write_all(b"\x1b[H")
35 } else if col == 0 {
36 write!(w, "\x1b[{}H", row + 1)
37 } else {
38 write!(w, "\x1b[{};{}H", row + 1, col + 1)
39 }
40}
41
42/// Move the cursor up `n` rows with CUU.
43///
44/// `n == 0` emits nothing, `n == 1` emits `ESC [ A`, and larger counts emit `ESC [ <n> A`.
45pub fn write_cuu<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
46 match n {
47 0 => Ok(()),
48 1 => w.write_all(b"\x1b[A"),
49 _ => write!(w, "\x1b[{n}A"),
50 }
51}
52
53/// Move the cursor down `n` rows with CUD.
54///
55/// `n == 0` emits nothing, `n == 1` emits `ESC [ B`, and larger counts emit `ESC [ <n> B`.
56pub fn write_cud<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
57 match n {
58 0 => Ok(()),
59 1 => w.write_all(b"\x1b[B"),
60 _ => write!(w, "\x1b[{n}B"),
61 }
62}
63
64/// Move the cursor forward/right `n` columns with CUF.
65///
66/// `n == 0` emits nothing, `n == 1` emits `ESC [ C`, and larger counts emit `ESC [ <n> C`.
67pub fn write_cuf<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
68 match n {
69 0 => Ok(()),
70 1 => w.write_all(b"\x1b[C"),
71 _ => write!(w, "\x1b[{n}C"),
72 }
73}
74
75/// Move the cursor backward/left `n` columns with CUB.
76///
77/// `n == 0` emits nothing, `n == 1` emits `ESC [ D`, and larger counts emit `ESC [ <n> D`.
78pub fn write_cub<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
79 match n {
80 0 => Ok(()),
81 1 => w.write_all(b"\x1b[D"),
82 _ => write!(w, "\x1b[{n}D"),
83 }
84}
85
86/// Move the cursor to an absolute column with CHA, `ESC [ <col+1> G`.
87///
88/// `col` is zero-based; `col == 0` emits the shorter `ESC [ G`.
89pub fn write_cha<W: Write>(w: &mut W, col: u16) -> io::Result<()> {
90 if col == 0 {
91 w.write_all(b"\x1b[G")
92 } else {
93 write!(w, "\x1b[{}G", col + 1)
94 }
95}
96
97/// Move the cursor to an absolute row with VPA, `ESC [ <row+1> d`.
98///
99/// `row` is zero-based; `row == 0` emits the shorter `ESC [ d`.
100pub fn write_vpa<W: Write>(w: &mut W, row: u16) -> io::Result<()> {
101 if row == 0 {
102 w.write_all(b"\x1b[d")
103 } else {
104 write!(w, "\x1b[{}d", row + 1)
105 }
106}
107
108/// Move to the first column of a following line with CNL.
109///
110/// `n == 0` emits nothing, `n == 1` emits `ESC [ E`, and larger counts emit `ESC [ <n> E`.
111pub fn write_cnl<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
112 match n {
113 0 => Ok(()),
114 1 => w.write_all(b"\x1b[E"),
115 _ => write!(w, "\x1b[{n}E"),
116 }
117}
118
119/// Move to the first column of a preceding line with CPL.
120///
121/// `n == 0` emits nothing, `n == 1` emits `ESC [ F`, and larger counts emit `ESC [ <n> F`.
122pub fn write_cpl<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
123 match n {
124 0 => Ok(()),
125 1 => w.write_all(b"\x1b[F"),
126 _ => write!(w, "\x1b[{n}F"),
127 }
128}
129
130/// Save the cursor with the DEC form `ESC 7` (DECSC).
131///
132/// This is distinct from [`write_save_cursor_position`], which emits the CSI `s` form.
133pub fn write_save_cursor<W: Write>(w: &mut W) -> io::Result<()> {
134 w.write_all(b"\x1b7")
135}
136
137/// Restore the cursor with the DEC form `ESC 8` (DECRC).
138///
139/// This restores the state saved by [`write_save_cursor`] on terminals that support DECSC/DECRC.
140pub fn write_restore_cursor<W: Write>(w: &mut W) -> io::Result<()> {
141 w.write_all(b"\x1b8")
142}
143
144/// Move up one line with Reverse Index, `ESC M`.
145///
146/// If the cursor is at the top margin, terminals scroll the region down. The single-byte 8-bit C1 equivalent is [`crate::ansi::c1::RI`].
147pub fn write_reverse_index<W: Write>(w: &mut W) -> io::Result<()> {
148 w.write_all(b"\x1bM")
149}
150
151/// Move down one line with Index, `ESC D`.
152///
153/// If the cursor is at the bottom margin, terminals scroll the region up. The single-byte 8-bit C1 equivalent is [`crate::ansi::c1::IND`].
154pub fn write_index<W: Write>(w: &mut W) -> io::Result<()> {
155 w.write_all(b"\x1bD")
156}
157
158/// Move the cursor with HVP, `ESC [ <row+1> ; <col+1> f`.
159///
160/// Arguments are zero-based API coordinates. The writer uses the same omission rules as [`write_cup`] but with final byte `f`.
161pub fn write_hvp<W: Write>(w: &mut W, row: u16, col: u16) -> io::Result<()> {
162 if row == 0 && col == 0 {
163 w.write_all(b"\x1b[f")
164 } else if col == 0 {
165 write!(w, "\x1b[{}f", row + 1)
166 } else {
167 write!(w, "\x1b[{};{}f", row + 1, col + 1)
168 }
169}
170
171/// Move to an absolute horizontal position with HPA, ``ESC [ <col+1> ` ``.
172///
173/// `col` is zero-based; `col == 0` emits the shorter ``ESC [ ` ``.
174pub fn write_hpa<W: Write>(w: &mut W, col: u16) -> io::Result<()> {
175 if col == 0 {
176 w.write_all(b"\x1b[`")
177 } else {
178 write!(w, "\x1b[{}`", col + 1)
179 }
180}
181
182/// Move horizontally right by `n` columns with HPR, `ESC [ <n> a`.
183///
184/// `n == 0` emits nothing and `n == 1` omits the parameter.
185pub fn write_hpr<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
186 match n {
187 0 => Ok(()),
188 1 => w.write_all(b"\x1b[a"),
189 _ => write!(w, "\x1b[{n}a"),
190 }
191}
192
193/// Move vertically down by `n` rows with VPR, `ESC [ <n> e`.
194///
195/// `n == 0` emits nothing and `n == 1` omits the parameter.
196pub fn write_vpr<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
197 match n {
198 0 => Ok(()),
199 1 => w.write_all(b"\x1b[e"),
200 _ => write!(w, "\x1b[{n}e"),
201 }
202}
203
204/// Advance `n` horizontal tab stops with CHT, `ESC [ <n> I`.
205///
206/// `n == 0` emits nothing and `n == 1` emits `ESC [ I`.
207pub fn write_cht<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
208 match n {
209 0 => Ok(()),
210 1 => w.write_all(b"\x1b[I"),
211 _ => write!(w, "\x1b[{n}I"),
212 }
213}
214
215/// Save the cursor position with CSI `s`, exact bytes `ESC [ s`.
216///
217/// This is the alternate save form; [`write_save_cursor`] emits DEC `ESC 7`.
218pub fn write_save_cursor_position<W: Write>(w: &mut W) -> io::Result<()> {
219 w.write_all(b"\x1b[s")
220}
221
222/// Restore the cursor position with CSI `u`, exact bytes `ESC [ u`.
223///
224/// This is the alternate restore form; [`write_restore_cursor`] emits DEC `ESC 8`.
225pub fn write_restore_cursor_position<W: Write>(w: &mut W) -> io::Result<()> {
226 w.write_all(b"\x1b[u")
227}
228
229/// Set the pointer shape with OSC 22, `ESC ] 22 ; <shape> ESC \`.
230///
231/// `shape` is emitted verbatim. The sequence uses the `ST` terminator rather than BEL.
232pub fn write_set_pointer_shape<W: Write>(w: &mut W, shape: &str) -> io::Result<()> {
233 write!(w, "\x1b]22;{shape}\x1b\\")
234}
235
236/// Request an extended cursor-position report with `ESC [ ? 6 n` (DECXCPR).
237///
238/// Compatible terminals reply with a private cursor-position report such as `ESC [ ? <line> ; <column> R`.
239pub fn write_request_extended_cursor_position<W: Write>(w: &mut W) -> io::Result<()> {
240 w.write_all(b"\x1b[?6n")
241}
242
243/// The visual shape of the text cursor, independent of whether it blinks.
244///
245/// DECSCUSR interleaves the two: each shape has a blinking and a steady
246/// parameter. [`CursorStyle::new`] combines them, and
247/// [`CursorStyle::shape`] / [`CursorStyle::blinking`] take them apart again.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
249pub enum CursorShape {
250 /// A full-cell block.
251 #[default]
252 Block,
253 /// A horizontal underline at the bottom of the cell.
254 Underline,
255 /// A vertical bar at the left of the cell.
256 Bar,
257}
258
259/// Cursor style values for DECSCUSR (`ESC [ Ps SP q`).
260///
261/// The variants map directly to the numeric `Ps` parameter used by
262/// [`write_cursor_style`].
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
264pub enum CursorStyle {
265 /// Terminal default, DECSCUSR parameter `0`.
266 ///
267 /// Writing this asks the terminal for the cursor its user configured.
268 /// kitty, foot, rio, Alacritty, VTE, Konsole and Windows Terminal all
269 /// read the shape back from configuration here, so it is the way to
270 /// hand the cursor back at the end of a session. xterm documents `0` as
271 /// a blinking block instead, which matches none of them.
272 ///
273 /// What it draws is therefore not knowable from the parameter: it is
274 /// whatever that user chose, and terminals do not even agree on the
275 /// blink, with kitty and Windows Terminal forcing it on where the
276 /// others restore it from configuration too. So this names no shape and
277 /// no blink state, and [`shape`](CursorStyle::shape) and
278 /// [`blinking`](CursorStyle::blinking) answer `None` for it. Ask with
279 /// [`Program::request_cursor_style`](crate::program::Program::request_cursor_style)
280 /// when you need to know what the cursor actually is.
281 #[default]
282 Default,
283 /// Blinking block cursor, DECSCUSR parameter `1`.
284 BlinkingBlock,
285 /// Steady block cursor, DECSCUSR parameter `2`.
286 SteadyBlock,
287 /// Blinking underline cursor, DECSCUSR parameter `3`.
288 BlinkingUnderline,
289 /// Steady underline cursor, DECSCUSR parameter `4`.
290 SteadyUnderline,
291 /// Blinking bar cursor, DECSCUSR parameter `5`.
292 BlinkingBar,
293 /// Steady bar cursor, DECSCUSR parameter `6`.
294 SteadyBar,
295}
296
297impl CursorStyle {
298 fn param(self) -> u8 {
299 match self {
300 CursorStyle::Default => 0,
301 CursorStyle::BlinkingBlock => 1,
302 CursorStyle::SteadyBlock => 2,
303 CursorStyle::BlinkingUnderline => 3,
304 CursorStyle::SteadyUnderline => 4,
305 CursorStyle::BlinkingBar => 5,
306 CursorStyle::SteadyBar => 6,
307 }
308 }
309
310 /// The style a DECSCUSR parameter selects.
311 ///
312 /// # Returns
313 ///
314 /// `None` for a parameter outside `0..=6`, which selects no style this
315 /// library knows. Used to read the style back out of a DECRPSS reply.
316 pub fn from_param(n: u32) -> Option<Self> {
317 Some(match n {
318 0 => CursorStyle::Default,
319 1 => CursorStyle::BlinkingBlock,
320 2 => CursorStyle::SteadyBlock,
321 3 => CursorStyle::BlinkingUnderline,
322 4 => CursorStyle::SteadyUnderline,
323 5 => CursorStyle::BlinkingBar,
324 6 => CursorStyle::SteadyBar,
325 _ => return None,
326 })
327 }
328
329 /// The style that draws `shape`, blinking or steady.
330 pub fn new(shape: CursorShape, blinking: bool) -> Self {
331 match (shape, blinking) {
332 (CursorShape::Block, true) => CursorStyle::BlinkingBlock,
333 (CursorShape::Block, false) => CursorStyle::SteadyBlock,
334 (CursorShape::Underline, true) => CursorStyle::BlinkingUnderline,
335 (CursorShape::Underline, false) => CursorStyle::SteadyUnderline,
336 (CursorShape::Bar, true) => CursorStyle::BlinkingBar,
337 (CursorShape::Bar, false) => CursorStyle::SteadyBar,
338 }
339 }
340
341 /// The shape this style draws.
342 ///
343 /// # Returns
344 ///
345 /// `None` for [`CursorStyle::Default`], which defers to the terminal
346 /// rather than naming a shape.
347 pub fn shape(self) -> Option<CursorShape> {
348 Some(match self {
349 CursorStyle::Default => return None,
350 CursorStyle::BlinkingBlock | CursorStyle::SteadyBlock => CursorShape::Block,
351 CursorStyle::BlinkingUnderline | CursorStyle::SteadyUnderline => CursorShape::Underline,
352 CursorStyle::BlinkingBar | CursorStyle::SteadyBar => CursorShape::Bar,
353 })
354 }
355
356 /// Whether this style blinks.
357 ///
358 /// # Returns
359 ///
360 /// `None` for [`CursorStyle::Default`], which defers to the terminal
361 /// rather than choosing.
362 pub fn blinking(self) -> Option<bool> {
363 Some(match self {
364 CursorStyle::Default => return None,
365 CursorStyle::BlinkingBlock
366 | CursorStyle::BlinkingUnderline
367 | CursorStyle::BlinkingBar => true,
368 CursorStyle::SteadyBlock | CursorStyle::SteadyUnderline | CursorStyle::SteadyBar => {
369 false
370 }
371 })
372 }
373}
374
375/// Set the cursor style with DECSCUSR, `ESC [ <style> SP q`.
376///
377/// The numeric parameter comes from [`CursorStyle`]: `0` for default, `1/2` block, `3/4` underline, and `5/6` bar.
378pub fn write_cursor_style<W: Write>(w: &mut W, style: CursorStyle) -> io::Result<()> {
379 write!(w, "\x1b[{} q", style.param())
380}
381
382/// Request a standard cursor-position report with `ESC [ 6 n` (DSR 6).
383///
384/// Compatible terminals reply with `ESC [ <line> ; <column> R`, using one-based coordinates.
385pub fn write_request_cursor_position<W: Write>(w: &mut W) -> io::Result<()> {
386 w.write_all(b"\x1b[6n")
387}
388
389/// Write a literal horizontal tab byte, `HT` (`0x09`).
390///
391/// This is not a CSI sequence; the terminal advances according to its current tab stops.
392pub fn write_tab<W: Write>(w: &mut W) -> io::Result<()> {
393 w.write_all(b"\t")
394}
395
396/// Move backward by `n` tab stops with CBT, `ESC [ <n> Z`.
397///
398/// `n == 0` emits nothing, `n == 1` emits `ESC [ Z`, and larger counts include the decimal count.
399pub fn write_backtab<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
400 match n {
401 0 => Ok(()),
402 1 => w.write_all(b"\x1b[Z"),
403 _ => write!(w, "\x1b[{n}Z"),
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 #[test]
412 fn cursor_style_round_trips_its_parameter() {
413 // `from_param` reads back what DECSCUSR wrote, which is what makes a
414 // DECRPSS reply decodable.
415 for style in [
416 CursorStyle::Default,
417 CursorStyle::BlinkingBlock,
418 CursorStyle::SteadyBlock,
419 CursorStyle::BlinkingUnderline,
420 CursorStyle::SteadyUnderline,
421 CursorStyle::BlinkingBar,
422 CursorStyle::SteadyBar,
423 ] {
424 assert_eq!(CursorStyle::from_param(style.param() as u32), Some(style));
425 }
426 assert_eq!(CursorStyle::from_param(7), None);
427 assert_eq!(CursorStyle::from_param(u32::MAX), None);
428 }
429
430 #[test]
431 fn shape_and_blinking_round_trip_through_a_style() {
432 for shape in [CursorShape::Block, CursorShape::Underline, CursorShape::Bar] {
433 for blinking in [true, false] {
434 let style = CursorStyle::new(shape, blinking);
435 assert_eq!(style.shape(), Some(shape));
436 assert_eq!(style.blinking(), Some(blinking));
437 }
438 }
439 }
440
441 #[test]
442 fn the_terminal_default_names_neither_shape_nor_blink() {
443 // DECSCUSR 0 defers to the terminal, so there is nothing to report.
444 assert_eq!(CursorStyle::Default.shape(), None);
445 assert_eq!(CursorStyle::Default.blinking(), None);
446 }
447
448 #[test]
449 fn test_cup_origin() {
450 let mut buf = Vec::new();
451 write_cup(&mut buf, 0, 0).unwrap();
452 assert_eq!(buf, b"\x1b[H");
453 }
454
455 #[test]
456 fn test_cup_row_only() {
457 let mut buf = Vec::new();
458 write_cup(&mut buf, 5, 0).unwrap();
459 assert_eq!(buf, b"\x1b[6H");
460 }
461
462 #[test]
463 fn test_cup_both() {
464 let mut buf = Vec::new();
465 write_cup(&mut buf, 10, 20).unwrap();
466 assert_eq!(buf, b"\x1b[11;21H");
467 }
468
469 #[test]
470 fn test_cuu_single() {
471 let mut buf = Vec::new();
472 write_cuu(&mut buf, 1).unwrap();
473 assert_eq!(buf, b"\x1b[A");
474 }
475
476 #[test]
477 fn test_cuu_multi() {
478 let mut buf = Vec::new();
479 write_cuu(&mut buf, 5).unwrap();
480 assert_eq!(buf, b"\x1b[5A");
481 }
482
483 #[test]
484 fn test_cursor_style() {
485 let mut buf = Vec::new();
486 write_cursor_style(&mut buf, CursorStyle::SteadyBar).unwrap();
487 assert_eq!(buf, b"\x1b[6 q");
488 }
489
490 #[test]
491 fn test_cup_cost() {
492 use crate::ansi::cost::cup_cost;
493 assert_eq!(cup_cost(0, 0), 3); // \x1b[H
494 assert_eq!(cup_cost(9, 0), 5); // \x1b[10H
495 assert_eq!(cup_cost(9, 9), 8); // \x1b[10;10H
496 }
497
498 // --- DECSCUSR encoding tests ---
499 //
500 // `CursorStyle` merges shape+blink into a single enum; each variant
501 // maps to a stable DECSCUSR parameter emitted by
502 // `write_cursor_style`. The mapping is exercised by writing each
503 // variant and asserting the emitted byte stream matches
504 // `CSI <param> SP q`.
505
506 #[test]
507 fn cursor_style_decscusr_encoding() {
508 for (style, param) in [
509 (CursorStyle::BlinkingBlock, 1),
510 (CursorStyle::SteadyBlock, 2),
511 (CursorStyle::BlinkingUnderline, 3),
512 (CursorStyle::SteadyUnderline, 4),
513 (CursorStyle::BlinkingBar, 5),
514 (CursorStyle::SteadyBar, 6),
515 ] {
516 let mut buf: Vec<u8> = Vec::new();
517 write_cursor_style(&mut buf, style).unwrap();
518 let want = format!("\x1b[{param} q");
519 assert_eq!(String::from_utf8(buf).unwrap(), want, "style {style:?}");
520 }
521 }
522}