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}