Skip to main content

uncurses/ansi/
screen.rs

1//! Screen, line, scroll-region, and tab-stop manipulation.
2//!
3//! ## Category
4//!
5//! This module emits CSI controls for erasing, inserting/deleting characters and
6//! lines, scrolling, setting vertical and horizontal margins, and tab-stop
7//! management.
8//!
9//! ## CSI conventions
10//!
11//! Counted operations use the terminal default parameter where possible: for
12//! example `n <= 1` emits `ESC [ X` for ECH and `ESC [ L` for IL. Erase
13//! operations use parameter `0` as the omitted default.
14//!
15//! ## Mode interaction
16//!
17//! Left/right margins are interpreted as DECSLRM when
18//! [`Mode::LEFT_RIGHT_MARGIN`](crate::ansi::mode::Mode::LEFT_RIGHT_MARGIN) is
19//! enabled. Top/bottom scroll margins are set by DECSTBM and affect absolute
20//! cursor movement when origin mode is active.
21
22use std::io::{self, Write};
23
24/// Erase `n` character cells at the cursor with ECH.
25///
26/// `n <= 1` emits `ESC [ X`; larger counts emit `ESC [ <n> X`. Erased cells are replaced with blank cells using the active rendition.
27pub fn write_ech<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
28    if n <= 1 {
29        w.write_all(b"\x1b[X")
30    } else {
31        write!(w, "\x1b[{n}X")
32    }
33}
34
35/// Repeat the preceding printable character with REP.
36///
37/// `n <= 1` emits `ESC [ b`; larger counts emit `ESC [ <n> b`. Use only when the terminal supports REP and the preceding character is repeatable.
38pub fn write_rep<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
39    if n <= 1 {
40        w.write_all(b"\x1b[b")
41    } else {
42        write!(w, "\x1b[{n}b")
43    }
44}
45
46/// Insert `n` blank character cells at the cursor with ICH.
47///
48/// `n <= 1` emits `ESC [ @`; larger counts emit `ESC [ <n> @`. Existing cells shift right within the line.
49pub fn write_ich<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
50    if n <= 1 {
51        w.write_all(b"\x1b[@")
52    } else {
53        write!(w, "\x1b[{n}@")
54    }
55}
56
57/// Delete `n` character cells at the cursor with DCH.
58///
59/// `n <= 1` emits `ESC [ P`; larger counts emit `ESC [ <n> P`. Cells to the right shift left and blanks are inserted at the right edge.
60pub fn write_dch<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
61    if n <= 1 {
62        w.write_all(b"\x1b[P")
63    } else {
64        write!(w, "\x1b[{n}P")
65    }
66}
67
68/// Erase in line with EL, `ESC [ <n> K`.
69///
70/// `n == 0` emits `ESC [ K` (cursor through end of line), `1` erases start through cursor, and `2` erases the entire line. Other values are emitted as provided.
71pub fn write_el<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
72    match n {
73        0 => w.write_all(b"\x1b[K"),
74        _ => write!(w, "\x1b[{n}K"),
75    }
76}
77
78/// Erase in display with ED, `ESC [ <n> J`.
79///
80/// `n == 0` emits `ESC [ J` (cursor through end of screen), `1` erases start through cursor, `2` erases the visible screen, and `3` requests scrollback/display clearing where supported.
81pub fn write_ed<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
82    match n {
83        0 => w.write_all(b"\x1b[J"),
84        _ => write!(w, "\x1b[{n}J"),
85    }
86}
87
88/// Erase from cursor through end of line: exact bytes `ESC [ K` (`EL 0`).
89pub const ERASE_LINE_RIGHT: &[u8] = b"\x1b[K";
90/// Erase from start of line through cursor: exact bytes `ESC [ 1 K` (`EL 1`).
91pub const ERASE_LINE_LEFT: &[u8] = b"\x1b[1K";
92/// Erase the entire current line: exact bytes `ESC [ 2 K` (`EL 2`).
93pub const ERASE_ENTIRE_LINE: &[u8] = b"\x1b[2K";
94/// Erase from cursor through end of screen: exact bytes `ESC [ J` (`ED 0`).
95pub const ERASE_SCREEN_BELOW: &[u8] = b"\x1b[J";
96/// Erase from start of screen through cursor: exact bytes `ESC [ 1 J` (`ED 1`).
97pub const ERASE_SCREEN_ABOVE: &[u8] = b"\x1b[1J";
98/// Erase the visible screen: exact bytes `ESC [ 2 J` (`ED 2`).
99pub const ERASE_ENTIRE_SCREEN: &[u8] = b"\x1b[2J";
100/// Erase the display including scrollback where supported: exact bytes `ESC [ 3 J` (`ED 3`).
101pub const ERASE_ENTIRE_DISPLAY: &[u8] = b"\x1b[3J";
102
103/// Write [`ERASE_LINE_RIGHT`], `ESC [ K`, erasing from cursor through end of line.
104pub fn write_erase_to_eol<W: Write>(w: &mut W) -> io::Result<()> {
105    w.write_all(ERASE_LINE_RIGHT)
106}
107
108/// Write [`ERASE_ENTIRE_LINE`], `ESC [ 2 K`, erasing the entire current line.
109pub fn write_erase_line<W: Write>(w: &mut W) -> io::Result<()> {
110    w.write_all(ERASE_ENTIRE_LINE)
111}
112
113/// Write [`ERASE_SCREEN_BELOW`], `ESC [ J`, erasing from cursor through end of screen.
114pub fn write_erase_below<W: Write>(w: &mut W) -> io::Result<()> {
115    w.write_all(ERASE_SCREEN_BELOW)
116}
117
118/// Write [`ERASE_ENTIRE_SCREEN`], `ESC [ 2 J`, erasing the visible screen.
119pub fn write_erase_screen<W: Write>(w: &mut W) -> io::Result<()> {
120    w.write_all(ERASE_ENTIRE_SCREEN)
121}
122
123/// Insert `n` blank lines with IL.
124///
125/// `n <= 1` emits `ESC [ L`; larger counts emit `ESC [ <n> L`. Lines below shift downward within the scrolling region.
126pub fn write_insert_lines<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
127    if n <= 1 {
128        w.write_all(b"\x1b[L")
129    } else {
130        write!(w, "\x1b[{n}L")
131    }
132}
133
134/// Delete `n` lines with DL.
135///
136/// `n <= 1` emits `ESC [ M`; larger counts emit `ESC [ <n> M`. Lines below shift upward within the scrolling region.
137pub fn write_delete_lines<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
138    if n <= 1 {
139        w.write_all(b"\x1b[M")
140    } else {
141        write!(w, "\x1b[{n}M")
142    }
143}
144
145/// Scroll up by `n` lines with SU.
146///
147/// `n <= 1` emits `ESC [ S`; larger counts emit `ESC [ <n> S`.
148pub fn write_scroll_up<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
149    if n <= 1 {
150        w.write_all(b"\x1b[S")
151    } else {
152        write!(w, "\x1b[{n}S")
153    }
154}
155
156/// Scroll down by `n` lines with SD.
157///
158/// `n <= 1` emits `ESC [ T`; larger counts emit `ESC [ <n> T`.
159pub fn write_scroll_down<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
160    if n <= 1 {
161        w.write_all(b"\x1b[T")
162    } else {
163        write!(w, "\x1b[{n}T")
164    }
165}
166
167/// Set top and bottom margins with DECSTBM, `ESC [ <top+1> ; <bottom+1> r`.
168///
169/// `top` and `bottom` are zero-based API row indices; the terminal parameters are one-based.
170pub fn write_scroll_region<W: Write>(w: &mut W, top: u16, bottom: u16) -> io::Result<()> {
171    write!(w, "\x1b[{};{}r", top + 1, bottom + 1)
172}
173
174/// Reset top/bottom margins to the full screen with exact bytes `ESC [ r`.
175pub fn write_reset_scroll_region<W: Write>(w: &mut W) -> io::Result<()> {
176    w.write_all(b"\x1b[r")
177}
178
179/// Set left and right margins with DECSLRM, `ESC [ <left> ; <right> s`.
180///
181/// Arguments are emitted as one-based terminal parameters where `0` means omitted/default. When both are `0`, this emits `ESC [ s`, the same byte sequence as the alternate save-cursor form outside DECSLRM context.
182pub fn write_set_left_right_margins<W: Write>(w: &mut W, left: u16, right: u16) -> io::Result<()> {
183    match (left, right) {
184        (0, 0) => w.write_all(b"\x1b[s"),
185        (l, 0) => write!(w, "\x1b[{l}s"),
186        (0, r) => write!(w, "\x1b[;{r}s"),
187        (l, r) => write!(w, "\x1b[{l};{r}s"),
188    }
189}
190
191/// Set tab stops every eight columns: exact bytes `ESC [ ? 5 W` (DECST8C).
192pub const SET_TAB_EVERY_8_COLUMNS: &[u8] = b"\x1b[?5W";
193
194/// Set a horizontal tab stop at the current column: exact bytes `ESC H` (HTS 7-bit form).
195pub const HORIZONTAL_TAB_SET: &[u8] = b"\x1bH";
196
197/// Write [`HORIZONTAL_TAB_SET`], `ESC H`, to set a tab stop at the current cursor column.
198pub fn write_hts<W: Write>(w: &mut W) -> io::Result<()> {
199    w.write_all(HORIZONTAL_TAB_SET)
200}
201
202/// Clear tab stops with TBC, `ESC [ <n> g`.
203///
204/// `n == 0` emits `ESC [ g` and clears the current-column tab stop. `n == 3` clears all tab stops; other values are emitted as provided.
205pub fn write_tbc<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
206    match n {
207        0 => w.write_all(b"\x1b[g"),
208        _ => write!(w, "\x1b[{n}g"),
209    }
210}
211
212/// Reset tab stops to one every eight columns without DECST8C.
213///
214/// Clears every existing stop with TBC and re-establishes one at each
215/// eighth column through `width` using HTS. The cursor is parked at
216/// column zero with CR before and after, and nothing is printed, so the
217/// visible row is left untouched. This is the portable fallback for
218/// terminals that do not implement [`SET_TAB_EVERY_8_COLUMNS`].
219///
220/// `width` is the managed width in cells; a `width` of eight or fewer
221/// only clears stops, since the first default stop already lies at or
222/// past the right edge.
223pub fn write_reset_tab_stops_every_8<W: Write>(w: &mut W, width: u16) -> io::Result<()> {
224    // Snap to a known reference column before clearing stops.
225    w.write_all(b"\r")?;
226    write_tbc(w, 3)?;
227    let mut col = 8u16;
228    let mut moved = false;
229    while col < width {
230        super::cursor::write_cuf(w, 8)?;
231        write_hts(w)?;
232        moved = true;
233        col += 8;
234    }
235    // Return to column zero so the inline cursor is left where it started.
236    if moved {
237        w.write_all(b"\r")?;
238    }
239    Ok(())
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_erase_to_eol() {
248        let mut buf = Vec::new();
249        write_erase_to_eol(&mut buf).unwrap();
250        assert_eq!(buf, b"\x1b[K");
251    }
252
253    #[test]
254    fn test_scroll_region() {
255        let mut buf = Vec::new();
256        write_scroll_region(&mut buf, 5, 20).unwrap();
257        assert_eq!(buf, b"\x1b[6;21r");
258    }
259
260    #[test]
261    fn test_el_variants() {
262        let mut buf = Vec::new();
263        write_el(&mut buf, 0).unwrap();
264        write_el(&mut buf, 1).unwrap();
265        write_el(&mut buf, 2).unwrap();
266        assert_eq!(buf, b"\x1b[K\x1b[1K\x1b[2K");
267    }
268
269    #[test]
270    fn test_ed_variants() {
271        let mut buf = Vec::new();
272        write_ed(&mut buf, 0).unwrap();
273        write_ed(&mut buf, 1).unwrap();
274        write_ed(&mut buf, 2).unwrap();
275        write_ed(&mut buf, 3).unwrap();
276        assert_eq!(buf, b"\x1b[J\x1b[1J\x1b[2J\x1b[3J");
277    }
278
279    #[test]
280    fn test_reset_tab_stops_every_8_clears_then_sets() {
281        let mut buf = Vec::new();
282        write_reset_tab_stops_every_8(&mut buf, 20).unwrap();
283        // CR, TBC clear-all, then CUF 8 + HTS for columns 8 and 16, then CR.
284        assert_eq!(buf, b"\r\x1b[3g\x1b[8C\x1bH\x1b[8C\x1bH\r");
285    }
286
287    #[test]
288    fn test_reset_tab_stops_every_8_narrow_only_clears() {
289        // Width 8 or less has no interior stop, so nothing moves the cursor
290        // and only the clear is emitted (no trailing CR).
291        let mut buf = Vec::new();
292        write_reset_tab_stops_every_8(&mut buf, 8).unwrap();
293        assert_eq!(buf, b"\r\x1b[3g");
294    }
295
296    #[test]
297    fn test_decslrm() {
298        let mut buf = Vec::new();
299        write_set_left_right_margins(&mut buf, 5, 70).unwrap();
300        assert_eq!(buf, b"\x1b[5;70s");
301    }
302
303    #[test]
304    fn test_tab_stops() {
305        let mut buf = Vec::new();
306        write_hts(&mut buf).unwrap();
307        write_tbc(&mut buf, 0).unwrap();
308        write_tbc(&mut buf, 3).unwrap();
309        assert_eq!(buf, b"\x1bH\x1b[g\x1b[3g");
310    }
311}