@@ -59,29 +59,12 @@ fn is_caret_control(b: u8) -> bool {
5959 matches ! ( b, 0x01 ..=0x1f if !matches!( b, b'\n' | b'\r' | 0x1b ) ) || b == 0x7f
6060}
6161
62- /// ncurses terminfo string escaping (the inverse of `tic`), reproducing `_nc_tic_expand`'s
63- /// byte-exact output as verified against real `tic`/`infocmp` across the whole terminfo DB:
64- /// * `\E` (ESC), `\n` (0x0a), `\r` (0x0d) -- always letter-escaped, regardless of context;
65- /// * byte 0x80 -> `\0`; other high bytes (0x81..=0xff) -> 3-digit octal `\nnn`;
66- /// * `\s` for a space that is the first byte OR part of the trailing all-space run
67- /// (interior spaces stay literal);
68- /// * `\,` and `\^` for comma/caret; backslash is doubled (`\\`) unless the previous source
69- /// byte is `^`, in which case it stays a single `\`;
70- /// * a `%` immediately followed by a printable byte takes that byte verbatim (so terminfo
71- /// operators like `%^`, `%:`, `%\` survive un-escaped), the lone exception being a comma,
72- /// which is always protected as `\,`;
73- /// * caret-eligible control bytes (see [`is_caret_control`]) use caret form `^X`
74- /// (`X = ch ^ 0x40`) when the value is "short" -- the summed escaped width of all *other*
75- /// bytes is <= 3 -- or (for `0x01..=0x1f`) when the next byte is an ASCII digit; otherwise
76- /// octal `\nnn`;
77- /// * all remaining printables (incl. `:`, `!`, `%`) are emitted literally.
78- fn tic_expand ( v : & [ u8 ] ) -> String {
79- let mut s = String :: new ( ) ;
80- // ncurses' "long value" test drives the caret-vs-octal choice for control bytes: sum the
81- // escaped width of every *non*-caret-control byte (control bytes themselves count as zero);
82- // once that exceeds 3, control bytes render as octal rather than caret. The walk mirrors the
83- // emission below so that a `%`-operator's verbatim byte is weighted as it actually renders
84- // (e.g. `%^` is two columns, not the three a standalone `\^` would cost).
62+ /// ncurses' "long value" test, driving the caret-vs-octal choice for control bytes: sum the escaped
63+ /// width of every *non*-caret-control byte (control bytes themselves count as zero); once that
64+ /// exceeds 3 -- or more than ten caret-eligible control bytes appear -- control bytes render as octal
65+ /// rather than caret. The walk mirrors the emission so that a `%`-operator's verbatim byte is
66+ /// weighted as it actually renders (e.g. `%^` is two columns, not the three a standalone `\^` costs).
67+ fn noncontrol_islong ( v : & [ u8 ] ) -> bool {
8568 let noncontrol_weight: usize = {
8669 let width = |b : u8 | -> usize {
8770 match b {
@@ -111,10 +94,29 @@ fn tic_expand(v: &[u8]) -> String {
11194 }
11295 w
11396 } ;
114- // A value packed with caret-eligible control bytes also tips "long": more than ten of them
115- // forces octal even when there is no other content to lengthen the estimate.
11697 let caret_control_count = v. iter ( ) . filter ( |& & b| is_caret_control ( b) ) . count ( ) ;
117- let islong = noncontrol_weight > 3 || caret_control_count > 10 ;
98+ noncontrol_weight > 3 || caret_control_count > 10
99+ }
100+
101+ /// ncurses terminfo string escaping (the inverse of `tic`), reproducing `_nc_tic_expand`'s
102+ /// byte-exact output as verified against real `tic`/`infocmp` across the whole terminfo DB:
103+ /// * `\E` (ESC), `\n` (0x0a), `\r` (0x0d) -- always letter-escaped, regardless of context;
104+ /// * byte 0x80 -> `\0`; other high bytes (0x81..=0xff) -> 3-digit octal `\nnn`;
105+ /// * `\s` for a space that is the first byte OR part of the trailing all-space run
106+ /// (interior spaces stay literal);
107+ /// * `\,` and `\^` for comma/caret; backslash is doubled (`\\`) unless the previous source
108+ /// byte is `^`, in which case it stays a single `\`;
109+ /// * a `%` immediately followed by a printable byte takes that byte verbatim (so terminfo
110+ /// operators like `%^`, `%:`, `%\` survive un-escaped), the lone exception being a comma,
111+ /// which is always protected as `\,`;
112+ /// * caret-eligible control bytes (see [`is_caret_control`]) use caret form `^X`
113+ /// (`X = ch ^ 0x40`) when the value is "short" -- the summed escaped width of all *other*
114+ /// bytes is <= 3 -- or (for `0x01..=0x1f`) when the next byte is an ASCII digit; otherwise
115+ /// octal `\nnn`;
116+ /// * all remaining printables (incl. `:`, `!`, `%`) are emitted literally.
117+ fn tic_expand ( v : & [ u8 ] ) -> String {
118+ let mut s = String :: new ( ) ;
119+ let islong = noncontrol_islong ( v) ;
118120
119121 let mut i = 0 ;
120122 while i < v. len ( ) {
@@ -211,7 +213,7 @@ fn termcap_code(name: &str, names: &[&str], codes: &[&str]) -> Option<String> {
211213
212214/// Escape one byte for a termcap value: like terminfo but `:` (the field separator) becomes `\072`,
213215/// and 0x7f is octal `\177` rather than caret.
214- fn tc_escape_byte ( out : & mut String , b : u8 ) {
216+ fn tc_escape_byte ( out : & mut String , b : u8 , islong : bool , next : Option < u8 > ) {
215217 match b {
216218 0x1b => out. push_str ( "\\ E" ) ,
217219 b'\n' => out. push_str ( "\\ n" ) ,
@@ -220,9 +222,17 @@ fn tc_escape_byte(out: &mut String, b: u8) {
220222 0x80 => out. push_str ( "\\ 0" ) ,
221223 0x7f => out. push_str ( "\\ 177" ) ,
222224 b'\\' => out. push_str ( "\\ \\ " ) ,
225+ // Control bytes follow the same caret-vs-octal "long value" heuristic as terminfo: caret
226+ // form `^X` for short values (or when the next byte is a digit, to avoid octal ambiguity),
227+ // octal `\nnn` once the value is long.
223228 0x00 ..=0x1f => {
224- out. push ( '^' ) ;
225- out. push ( ( b ^ 0x40 ) as char ) ;
229+ let next_is_digit = next. is_some_and ( |n| n. is_ascii_digit ( ) ) ;
230+ if !islong || next_is_digit {
231+ out. push ( '^' ) ;
232+ out. push ( ( b ^ 0x40 ) as char ) ;
233+ } else {
234+ out. push_str ( & format ! ( "\\ {b:03o}" ) ) ;
235+ }
226236 }
227237 0x81 ..=0xff => out. push_str ( & format ! ( "\\ {b:03o}" ) ) ,
228238 _ => out. push ( b as char ) ,
@@ -267,7 +277,7 @@ fn extract_padding(v: &[u8]) -> (Vec<u8>, String) {
267277/// `%2d`->`%2`, `%03d`->`%3`, `%c`->`%.`, `%i`, `%%`, `%'X'%+%c`->`%+X`, reversed args -> leading
268278/// `%r`). Returns `None` if any operator has no termcap equivalent (stack ops, `%?`/`%t`/`%e`/`%;`,
269279/// `%{`, `%x`, ...), in which case the caller keeps the value verbatim, exactly as ncurses does.
270- fn try_translate ( v : & [ u8 ] ) -> Option < String > {
280+ fn try_translate ( v : & [ u8 ] , islong : bool ) -> Option < String > {
271281 // Decide whether the two parameters are used in reverse order (`%p2` before `%p1`).
272282 let mut order: Vec < u8 > = Vec :: new ( ) ;
273283 let mut i = 0 ;
@@ -295,7 +305,7 @@ fn try_translate(v: &[u8]) -> Option<String> {
295305 let mut i = 0 ;
296306 while i < v. len ( ) {
297307 if v[ i] != b'%' {
298- tc_escape_byte ( & mut out, v[ i] ) ;
308+ tc_escape_byte ( & mut out, v[ i] , islong , v . get ( i + 1 ) . copied ( ) ) ;
299309 i += 1 ;
300310 continue ;
301311 }
@@ -357,7 +367,7 @@ fn try_translate(v: &[u8]) -> Option<String> {
357367 r_pending = false ;
358368 }
359369 out. push_str ( "%+" ) ;
360- tc_escape_byte ( & mut out, v[ i + 2 ] ) ;
370+ tc_escape_byte ( & mut out, v[ i + 2 ] , islong , v . get ( i + 3 ) . copied ( ) ) ;
361371 i += 8 ;
362372 }
363373 _ => return None ,
@@ -372,10 +382,11 @@ fn try_translate(v: &[u8]) -> Option<String> {
372382/// `_nc_infotocap`, which keeps untranslatable caps rather than dropping them.
373383fn tc_xlat ( raw : & [ u8 ] ) -> String {
374384 let ( v, delay) = extract_padding ( raw) ;
375- let body = try_translate ( & v) . unwrap_or_else ( || {
385+ let islong = noncontrol_islong ( & v) ;
386+ let body = try_translate ( & v, islong) . unwrap_or_else ( || {
376387 let mut s = String :: new ( ) ;
377- for & b in & v {
378- tc_escape_byte ( & mut s, b ) ;
388+ for i in 0 ..v . len ( ) {
389+ tc_escape_byte ( & mut s, v [ i ] , islong , v . get ( i + 1 ) . copied ( ) ) ;
379390 }
380391 s
381392 } ) ;
0 commit comments