Skip to main content

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}