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}