uncurses/ansi/cost.rs
1//! Byte-length predictors for emitted ANSI sequences.
2//!
3//! ## Category
4//!
5//! Cost functions mirror writer functions in [`crate::ansi::cursor`] and
6//! [`crate::ansi::screen`]. They let render planning compare equivalent cursor,
7//! erase, scroll, and overwrite strategies without allocating formatted strings.
8//!
9//! ## Conventions
10//!
11//! Costs are byte counts for the exact 7-bit sequences emitted by this crate,
12//! including omitted default parameters such as `ESC [ A` for `CUU 1` and
13//! `ESC [ H` for home.
14//!
15//! ## Mode interaction
16//!
17//! The predictors do not change terminal modes. For mode-sensitive sequences,
18//! such as left/right margins requiring [`Mode::LEFT_RIGHT_MARGIN`](crate::ansi::mode::Mode::LEFT_RIGHT_MARGIN), the cost still describes only the bytes emitted.
19
20/// Bytes in a CSI introducer (`ESC [`).
21const CSI_LEN: usize = 2;
22
23/// Number of decimal digits in a `u16`. Branchy table — faster than
24/// `ilog10` on the small range we feed it (cursor coordinates,
25/// repeat counts).
26pub const fn digit_count(n: u16) -> usize {
27 if n < 10 {
28 1
29 } else if n < 100 {
30 2
31 } else if n < 1000 {
32 3
33 } else if n < 10000 {
34 4
35 } else {
36 5
37 }
38}
39
40/// Cost of a single-parameter CSI where `n == 1` omits the parameter
41/// (the writer emits `ESC [ X` instead of `ESC [ 1 X`). Final byte
42/// contributes 1.
43///
44/// Shape: `\x1b[X` for `n <= 1`, `\x1b[{n}X` otherwise.
45const fn csi_optional_param_cost(n: u16) -> usize {
46 if n <= 1 {
47 CSI_LEN + 1
48 } else {
49 CSI_LEN + digit_count(n) + 1
50 }
51}
52
53/// Cost of a single-parameter CSI whose parameter is dropped when
54/// the 0-based input value is `0` (the canonical "absolute
55/// position" shape: CHA, HPA, VPA all default to row/col 1, which
56/// corresponds to a 0-based input of 0).
57///
58/// Shape: `\x1b[X` for `arg == 0`, `\x1b[{arg+1}X` otherwise.
59const fn csi_absolute_position_cost(arg: u16) -> usize {
60 if arg == 0 {
61 CSI_LEN + 1
62 } else {
63 CSI_LEN + digit_count(arg + 1) + 1
64 }
65}
66
67/// Cost of an erase-style CSI where the default parameter is `0`
68/// (the writer emits `ESC [ X` for `n == 0` and `ESC [ {n} X`
69/// otherwise).
70///
71/// Shape: `\x1b[X` for `n == 0`, `\x1b[{n}X` otherwise.
72const fn csi_erase_cost(n: u16) -> usize {
73 if n == 0 {
74 CSI_LEN + 1
75 } else {
76 CSI_LEN + digit_count(n) + 1
77 }
78}
79
80// ---------- Cursor movement -------------------------------------------------
81
82/// Cost of a CUP sequence for the given 0-based position. The writer
83/// drops the column when it is `0`, and drops both parameters when
84/// the destination is the origin.
85pub const fn cup_cost(row: u16, col: u16) -> usize {
86 if row == 0 && col == 0 {
87 // \x1b[H
88 CSI_LEN + 1
89 } else if col == 0 {
90 // \x1b[{row+1}H
91 CSI_LEN + digit_count(row + 1) + 1
92 } else {
93 // \x1b[{row+1};{col+1}H
94 CSI_LEN + digit_count(row + 1) + 1 + digit_count(col + 1) + 1
95 }
96}
97
98/// Cost of CUU (`\x1b[{n?}A`).
99pub const fn cuu_cost(n: u16) -> usize {
100 csi_optional_param_cost(n)
101}
102
103/// Cost of CUD (`\x1b[{n?}B`).
104pub const fn cud_cost(n: u16) -> usize {
105 csi_optional_param_cost(n)
106}
107
108/// Cost of CUF (`\x1b[{n?}C`).
109pub const fn cuf_cost(n: u16) -> usize {
110 csi_optional_param_cost(n)
111}
112
113/// Cost of CUB (`\x1b[{n?}D`).
114pub const fn cub_cost(n: u16) -> usize {
115 csi_optional_param_cost(n)
116}
117
118/// Cost of CHA (`\x1b[{col+1?}G`). The writer drops the parameter
119/// when `col == 0`.
120pub const fn cha_cost(col: u16) -> usize {
121 csi_absolute_position_cost(col)
122}
123
124/// Cost of HPA (``\x1b[{col+1?}` ``). The writer drops the parameter
125/// when `col == 0`.
126pub const fn hpa_cost(col: u16) -> usize {
127 csi_absolute_position_cost(col)
128}
129
130/// Cost of VPA (`\x1b[{row+1?}d`). The writer drops the parameter
131/// when `row == 0`.
132pub const fn vpa_cost(row: u16) -> usize {
133 csi_absolute_position_cost(row)
134}
135
136/// Cost of CHT (`\x1b[{n?}I`).
137pub const fn cht_cost(n: u16) -> usize {
138 csi_optional_param_cost(n)
139}
140
141/// Cost of CBT (`\x1b[{n?}Z`).
142pub const fn cbt_cost(n: u16) -> usize {
143 csi_optional_param_cost(n)
144}
145
146/// Cost of Reverse Index (`\x1bM`).
147pub const RI_COST: usize = 2;
148
149// ---------- Cell operations -------------------------------------------------
150
151/// Cost of ICH (`\x1b[{n?}@`).
152pub const fn ich_cost(n: u16) -> usize {
153 csi_optional_param_cost(n)
154}
155
156/// Cost of DCH (`\x1b[{n?}P`).
157pub const fn dch_cost(n: u16) -> usize {
158 csi_optional_param_cost(n)
159}
160
161/// Cost of ECH (`\x1b[{n?}X`).
162pub const fn ech_cost(n: u16) -> usize {
163 csi_optional_param_cost(n)
164}
165
166/// Cost of REP (`\x1b[{n?}b`).
167pub const fn rep_cost(n: u16) -> usize {
168 csi_optional_param_cost(n)
169}
170
171// ---------- Line operations -------------------------------------------------
172
173/// Cost of IL (`\x1b[{n?}L`).
174pub const fn il_cost(n: u16) -> usize {
175 csi_optional_param_cost(n)
176}
177
178/// Cost of DL (`\x1b[{n?}M`).
179pub const fn dl_cost(n: u16) -> usize {
180 csi_optional_param_cost(n)
181}
182
183/// Cost of SU (`\x1b[{n?}S`).
184pub const fn su_cost(n: u16) -> usize {
185 csi_optional_param_cost(n)
186}
187
188/// Cost of SD (`\x1b[{n?}T`).
189pub const fn sd_cost(n: u16) -> usize {
190 csi_optional_param_cost(n)
191}
192
193// ---------- Erase -----------------------------------------------------------
194
195/// Cost of EL (`\x1b[{n?}K`). `n == 0` omits the parameter.
196pub const fn el_cost(n: u16) -> usize {
197 csi_erase_cost(n)
198}
199
200/// Cost of ED (`\x1b[{n?}J`). `n == 0` omits the parameter.
201pub const fn ed_cost(n: u16) -> usize {
202 csi_erase_cost(n)
203}
204
205// ---------- Scroll region ---------------------------------------------------
206
207/// Cost of DECSTBM (`\x1b[{top+1};{bottom+1}r`).
208pub const fn decstbm_cost(top: u16, bottom: u16) -> usize {
209 // CSI + d(top+1) + ';' + d(bottom+1) + 'r'
210 CSI_LEN + digit_count(top + 1) + 1 + digit_count(bottom + 1) + 1
211}
212
213/// Cost of resetting DECSTBM to the full screen (`\x1b[r`).
214pub const DECSTBM_RESET_COST: usize = 3;
215
216// ---------- C0 / fixed prefixes --------------------------------------------
217
218/// Cost of a carriage return (`\r`).
219pub const CR_COST: usize = 1;
220
221/// Cost of an absolute home jump (`\x1b[H`).
222pub const HOME_COST: usize = 3;
223
224/// Cost of `n` literal line-feed bytes.
225pub const fn lf_cost(n: u16) -> usize {
226 n as usize
227}
228
229/// Cost of `n` literal backspace bytes.
230pub const fn bs_cost(n: u16) -> usize {
231 n as usize
232}
233
234/// Cost of `n` literal tab bytes.
235pub const fn tab_cost(n: u16) -> usize {
236 n as usize
237}
238
239// ---------- Overwrite (re-emit row cells as a forward move) ----------------
240
241/// Approximate byte cost of re-emitting the cells in `line[from..to]`
242/// as a forward-move candidate.
243///
244/// Returns [`None`] when any `width > 0` cell in the range has a
245/// style or link that does not match the active pen — in that case
246/// the row cannot be re-emitted without an interleaved pen change
247/// and the candidate is not eligible.
248///
249/// The cost approximation is the sum of `cell.width()` over occupied
250/// cells in the range. For plain ASCII this equals the emitted byte
251/// length exactly. For wide CJK glyphs (`width == 2`, multi-byte
252/// content) and other multi-byte single-width glyphs (combining
253/// sequences, emoji selectors) the prediction underestimates the
254/// emitted length, so an overwrite candidate may be picked here that
255/// a strict byte minimisation would have rejected. Accepting this
256/// approximation keeps the predictor allocation-free.
257pub fn overwrite_cost(
258 line: &[crate::cell::Cell],
259 style: &crate::style::Style,
260 from: u16,
261 to: u16,
262) -> Option<usize> {
263 let from = from as usize;
264 let to = to as usize;
265 if to > line.len() {
266 return None;
267 }
268 let mut i = from;
269 let mut cost = 0usize;
270 while i < to {
271 let cell = &line[i];
272 if !cell.is_continuation() {
273 if &cell.style != style {
274 return None;
275 }
276 cost += cell.width() as usize;
277 i += cell.width() as usize;
278 continue;
279 }
280 i += 1;
281 }
282 Some(cost)
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use crate::ansi::cursor::{
289 write_backtab, write_cha, write_cht, write_cub, write_cud, write_cuf, write_cup, write_cuu,
290 write_hpa, write_reverse_index, write_vpa,
291 };
292 use crate::ansi::screen::{
293 write_dch, write_delete_lines, write_ech, write_ed, write_el, write_ich,
294 write_insert_lines, write_rep, write_reset_scroll_region, write_scroll_down,
295 write_scroll_region, write_scroll_up,
296 };
297
298 /// Representative values exercising every branch of `digit_count`:
299 /// the `n == 0` / `n == 1` shortcuts, and 1..5 digit widths.
300 const NS: &[u16] = &[0, 1, 2, 9, 10, 99, 100, 999, 1000, 9999, 10000, u16::MAX];
301
302 fn write_to_vec(f: impl FnOnce(&mut Vec<u8>)) -> Vec<u8> {
303 let mut v = Vec::new();
304 f(&mut v);
305 v
306 }
307
308 #[test]
309 fn digit_count_matches_decimal_width() {
310 for &n in NS {
311 assert_eq!(digit_count(n), n.to_string().len(), "digit_count({n})");
312 }
313 }
314
315 #[test]
316 fn cup_cost_round_trips() {
317 let coords = [
318 (0, 0),
319 (0, 5),
320 (5, 0),
321 (5, 7),
322 (99, 99),
323 (999, 9),
324 (10, 1000),
325 ];
326 for (r, c) in coords {
327 let bytes = write_to_vec(|v| write_cup(v, r, c).unwrap());
328 assert_eq!(
329 cup_cost(r, c),
330 bytes.len(),
331 "cup_cost({r},{c}) vs {bytes:?}"
332 );
333 }
334 }
335
336 /// Drive the optional-param shape through every parameterized
337 /// writer that consumes it. Each call asserts that the cost
338 /// helper agrees with the writer's actual output.
339 #[test]
340 fn optional_param_costs_round_trip() {
341 for &n in NS {
342 // `write_cuu` and friends are no-ops for `n == 0`; that
343 // is the writer's own contract and we don't predict a
344 // cost for the empty emission.
345 if n == 0 {
346 continue;
347 }
348
349 let pairs: &[(&str, usize, Vec<u8>)] = &[
350 (
351 "cuu",
352 cuu_cost(n),
353 write_to_vec(|v| write_cuu(v, n).unwrap()),
354 ),
355 (
356 "cud",
357 cud_cost(n),
358 write_to_vec(|v| write_cud(v, n).unwrap()),
359 ),
360 (
361 "cuf",
362 cuf_cost(n),
363 write_to_vec(|v| write_cuf(v, n).unwrap()),
364 ),
365 (
366 "cub",
367 cub_cost(n),
368 write_to_vec(|v| write_cub(v, n).unwrap()),
369 ),
370 (
371 "cht",
372 cht_cost(n),
373 write_to_vec(|v| write_cht(v, n).unwrap()),
374 ),
375 (
376 "cbt",
377 cbt_cost(n),
378 write_to_vec(|v| write_backtab(v, n).unwrap()),
379 ),
380 (
381 "ich",
382 ich_cost(n),
383 write_to_vec(|v| write_ich(v, n).unwrap()),
384 ),
385 (
386 "dch",
387 dch_cost(n),
388 write_to_vec(|v| write_dch(v, n).unwrap()),
389 ),
390 (
391 "ech",
392 ech_cost(n),
393 write_to_vec(|v| write_ech(v, n).unwrap()),
394 ),
395 (
396 "rep",
397 rep_cost(n),
398 write_to_vec(|v| write_rep(v, n).unwrap()),
399 ),
400 (
401 "il",
402 il_cost(n),
403 write_to_vec(|v| write_insert_lines(v, n).unwrap()),
404 ),
405 (
406 "dl",
407 dl_cost(n),
408 write_to_vec(|v| write_delete_lines(v, n).unwrap()),
409 ),
410 (
411 "su",
412 su_cost(n),
413 write_to_vec(|v| write_scroll_up(v, n).unwrap()),
414 ),
415 (
416 "sd",
417 sd_cost(n),
418 write_to_vec(|v| write_scroll_down(v, n).unwrap()),
419 ),
420 ];
421 for (label, cost, bytes) in pairs {
422 assert_eq!(*cost, bytes.len(), "{label}({n}) bytes={bytes:?}");
423 }
424 }
425 }
426
427 #[test]
428 fn required_param_costs_round_trip() {
429 // u16::MAX overflows the writers' `arg + 1`; cap at 9999 (5
430 // digits exercise the wide branch). The `digit_count` test
431 // above already covers u16::MAX.
432 for &col in NS.iter().filter(|&&n| n <= 9999) {
433 let cha = write_to_vec(|v| write_cha(v, col).unwrap());
434 assert_eq!(cha_cost(col), cha.len(), "cha_cost({col})");
435 let hpa = write_to_vec(|v| write_hpa(v, col).unwrap());
436 assert_eq!(hpa_cost(col), hpa.len(), "hpa_cost({col})");
437 let vpa = write_to_vec(|v| write_vpa(v, col).unwrap());
438 assert_eq!(vpa_cost(col), vpa.len(), "vpa_cost({col})");
439 }
440 }
441
442 #[test]
443 fn erase_costs_round_trip() {
444 for n in 0u16..=3 {
445 let el = write_to_vec(|v| write_el(v, n).unwrap());
446 assert_eq!(el_cost(n), el.len(), "el_cost({n})");
447 let ed = write_to_vec(|v| write_ed(v, n).unwrap());
448 assert_eq!(ed_cost(n), ed.len(), "ed_cost({n})");
449 }
450 }
451
452 #[test]
453 fn decstbm_cost_round_trips() {
454 let cases = [(0u16, 0u16), (0, 23), (4, 19), (99, 199), (999, 9999)];
455 for (top, bottom) in cases {
456 let bytes = write_to_vec(|v| write_scroll_region(v, top, bottom).unwrap());
457 assert_eq!(
458 decstbm_cost(top, bottom),
459 bytes.len(),
460 "decstbm({top},{bottom})"
461 );
462 }
463 }
464
465 #[test]
466 fn decstbm_reset_cost_matches_writer() {
467 let bytes = write_to_vec(|v| write_reset_scroll_region(v).unwrap());
468 assert_eq!(DECSTBM_RESET_COST, bytes.len());
469 }
470
471 #[test]
472 fn ri_cost_matches_writer() {
473 let bytes = write_to_vec(|v| write_reverse_index(v).unwrap());
474 assert_eq!(RI_COST, bytes.len());
475 }
476}