Skip to main content

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}