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/// Cursor style values for DECSCUSR (`ESC [ Ps SP q`).
244///
245/// The variants map directly to the numeric `Ps` parameter used by
246/// [`write_cursor_style`].
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
248pub enum CursorStyle {
249    /// Use terminal default cursor style, DECSCUSR parameter `0`.
250    #[default]
251    Default,
252    /// Blinking block cursor, DECSCUSR parameter `1`.
253    BlinkingBlock,
254    /// Steady block cursor, DECSCUSR parameter `2`.
255    SteadyBlock,
256    /// Blinking underline cursor, DECSCUSR parameter `3`.
257    BlinkingUnderline,
258    /// Steady underline cursor, DECSCUSR parameter `4`.
259    SteadyUnderline,
260    /// Blinking bar cursor, DECSCUSR parameter `5`.
261    BlinkingBar,
262    /// Steady bar cursor, DECSCUSR parameter `6`.
263    SteadyBar,
264}
265
266impl CursorStyle {
267    fn param(self) -> u8 {
268        match self {
269            CursorStyle::Default => 0,
270            CursorStyle::BlinkingBlock => 1,
271            CursorStyle::SteadyBlock => 2,
272            CursorStyle::BlinkingUnderline => 3,
273            CursorStyle::SteadyUnderline => 4,
274            CursorStyle::BlinkingBar => 5,
275            CursorStyle::SteadyBar => 6,
276        }
277    }
278}
279
280/// Set the cursor style with DECSCUSR, `ESC [ <style> SP q`.
281///
282/// The numeric parameter comes from [`CursorStyle`]: `0` for default, `1/2` block, `3/4` underline, and `5/6` bar.
283pub fn write_cursor_style<W: Write>(w: &mut W, style: CursorStyle) -> io::Result<()> {
284    write!(w, "\x1b[{} q", style.param())
285}
286
287/// Request a standard cursor-position report with `ESC [ 6 n` (DSR 6).
288///
289/// Compatible terminals reply with `ESC [ <line> ; <column> R`, using one-based coordinates.
290pub fn write_request_cursor_position<W: Write>(w: &mut W) -> io::Result<()> {
291    w.write_all(b"\x1b[6n")
292}
293
294/// Write a literal horizontal tab byte, `HT` (`0x09`).
295///
296/// This is not a CSI sequence; the terminal advances according to its current tab stops.
297pub fn write_tab<W: Write>(w: &mut W) -> io::Result<()> {
298    w.write_all(b"\t")
299}
300
301/// Move backward by `n` tab stops with CBT, `ESC [ <n> Z`.
302///
303/// `n == 0` emits nothing, `n == 1` emits `ESC [ Z`, and larger counts include the decimal count.
304pub fn write_backtab<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
305    match n {
306        0 => Ok(()),
307        1 => w.write_all(b"\x1b[Z"),
308        _ => write!(w, "\x1b[{n}Z"),
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    #[test]
317    fn test_cup_origin() {
318        let mut buf = Vec::new();
319        write_cup(&mut buf, 0, 0).unwrap();
320        assert_eq!(buf, b"\x1b[H");
321    }
322
323    #[test]
324    fn test_cup_row_only() {
325        let mut buf = Vec::new();
326        write_cup(&mut buf, 5, 0).unwrap();
327        assert_eq!(buf, b"\x1b[6H");
328    }
329
330    #[test]
331    fn test_cup_both() {
332        let mut buf = Vec::new();
333        write_cup(&mut buf, 10, 20).unwrap();
334        assert_eq!(buf, b"\x1b[11;21H");
335    }
336
337    #[test]
338    fn test_cuu_single() {
339        let mut buf = Vec::new();
340        write_cuu(&mut buf, 1).unwrap();
341        assert_eq!(buf, b"\x1b[A");
342    }
343
344    #[test]
345    fn test_cuu_multi() {
346        let mut buf = Vec::new();
347        write_cuu(&mut buf, 5).unwrap();
348        assert_eq!(buf, b"\x1b[5A");
349    }
350
351    #[test]
352    fn test_cursor_style() {
353        let mut buf = Vec::new();
354        write_cursor_style(&mut buf, CursorStyle::SteadyBar).unwrap();
355        assert_eq!(buf, b"\x1b[6 q");
356    }
357
358    #[test]
359    fn test_cup_cost() {
360        use crate::ansi::cost::cup_cost;
361        assert_eq!(cup_cost(0, 0), 3); // \x1b[H
362        assert_eq!(cup_cost(9, 0), 5); // \x1b[10H
363        assert_eq!(cup_cost(9, 9), 8); // \x1b[10;10H
364    }
365
366    // --- DECSCUSR encoding tests ---
367    //
368    // `CursorStyle` merges shape+blink into a single enum; each variant
369    // maps to a stable DECSCUSR parameter emitted by
370    // `write_cursor_style`. The mapping is exercised by writing each
371    // variant and asserting the emitted byte stream matches
372    // `CSI <param> SP q`.
373
374    #[test]
375    fn cursor_style_decscusr_encoding() {
376        for (style, param) in [
377            (CursorStyle::BlinkingBlock, 1),
378            (CursorStyle::SteadyBlock, 2),
379            (CursorStyle::BlinkingUnderline, 3),
380            (CursorStyle::SteadyUnderline, 4),
381            (CursorStyle::BlinkingBar, 5),
382            (CursorStyle::SteadyBar, 6),
383        ] {
384            let mut buf: Vec<u8> = Vec::new();
385            write_cursor_style(&mut buf, style).unwrap();
386            let want = format!("\x1b[{param} q");
387            assert_eq!(String::from_utf8(buf).unwrap(), want, "style {style:?}");
388        }
389    }
390}