Skip to main content

zsh/
termcap.rs

1//! Termcap module - port of Modules/termcap.c
2//!
3//! Provides termcap manipulation through the echotc builtin and termcap hash.
4
5use std::collections::HashMap;
6
7/// Termcap boolean capability codes
8pub static BOOL_CODES: &[&str] = &[
9    "bw", "am", "ut", "cc", "xs", "YA", "YF", "YB", "xt", "xn", "eo", "gn", "hc", "HC", "km", "YC",
10    "hs", "hl", "in", "YG", "da", "db", "mi", "ms", "nx", "xb", "NP", "ND", "NR", "os", "5i", "YD",
11    "YE", "es", "hz", "ul", "xo",
12];
13
14/// Termcap numeric capability codes
15pub static NUM_CODES: &[&str] = &[
16    "co", "it", "lh", "lw", "li", "lm", "sg", "ma", "Co", "pa", "MW", "NC", "Nl", "pb", "vt", "ws",
17    "Yo", "Yp", "Ya", "BT", "Yc", "Yb", "Yd", "Ye", "Yf", "Yg", "Yh", "Yi", "Yk", "Yj", "Yl", "Ym",
18    "Yn",
19];
20
21/// Termcap string capability codes
22pub static STR_CODES: &[&str] = &[
23    "ac", "bt", "bl", "cr", "ZA", "ZB", "ZC", "ZD", "cs", "rP", "ct", "MC", "cl", "cb", "ce", "cd",
24    "ch", "CC", "CW", "cm", "do", "ho", "vi", "le", "CM", "ve", "nd", "ll", "up", "vs", "ZE", "dc",
25    "dl", "DI", "ds", "DK", "hd", "eA", "as", "SA", "mb", "md", "ti", "dm", "mh", "ZF", "ZG", "im",
26    "ZH", "ZI", "ZJ", "ZK", "ZL", "mp", "mr", "mk", "ZM", "so", "ZN", "ZO", "us", "ZP", "SX", "ec",
27    "ae", "RA", "me", "te", "ed", "ZQ", "ei", "ZR", "ZS", "ZT", "ZU", "se", "ZV", "ZW", "ue", "ZX",
28    "RX", "PA", "fh", "vb", "ff", "fs", "WG", "HU", "i1", "is", "i3", "if", "iP", "Ic", "Ip", "ic",
29    "al", "ip", "K1", "K3", "K2", "kb", "kB", "K4", "K5", "ka", "kC", "kt", "kD", "kL", "kd", "kM",
30    "kE", "kS", "k0", "k1", "k2", "k3", "k4", "k5", "k6", "k7", "k8", "k9", "kh", "kI", "kA", "kl",
31    "kH", "kN", "kP", "kr", "kF", "kR", "kT", "ku", "ke", "ks", "l0", "l1", "l2", "l3", "l4", "l5",
32    "l6", "l7", "l8", "l9", "nw", "oc", "op", "pc", "DC", "DL", "DO", "IC", "SF", "AL", "LE", "RI",
33    "SR", "UP", "pk", "pl", "px", "pn", "ps", "pO", "pf", "po", "rc", "cv", "sc", "sf", "sr", "sa",
34    "st", "ta", "ts", "uc", "hu",
35];
36
37/// Termcap capability value
38#[derive(Debug, Clone)]
39pub enum TermcapValue {
40    Boolean(bool),
41    Number(i32),
42    String(String),
43}
44
45/// Termcap interface using basic ANSI escape sequences
46#[derive(Debug, Default)]
47pub struct Termcap {
48    initialized: bool,
49    terminal: Option<String>,
50    capabilities: HashMap<String, TermcapValue>,
51}
52
53impl Termcap {
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    /// Initialize termcap for the given terminal
59    pub fn init(&mut self, term: Option<&str>) -> bool {
60        let terminal = term
61            .map(|s| s.to_string())
62            .or_else(|| std::env::var("TERM").ok());
63
64        if let Some(t) = terminal {
65            self.terminal = Some(t.clone());
66            self.load_capabilities(&t);
67            self.initialized = true;
68            return true;
69        }
70
71        false
72    }
73
74    fn load_capabilities(&mut self, term: &str) {
75        let is_xterm =
76            term.contains("xterm") || term.contains("256color") || term.contains("screen");
77        let is_ansi = is_xterm || term.contains("ansi") || term.contains("vt100");
78
79        self.capabilities
80            .insert("am".to_string(), TermcapValue::Boolean(true));
81        self.capabilities
82            .insert("km".to_string(), TermcapValue::Boolean(true));
83        self.capabilities
84            .insert("mi".to_string(), TermcapValue::Boolean(true));
85        self.capabilities
86            .insert("ms".to_string(), TermcapValue::Boolean(true));
87        self.capabilities
88            .insert("xn".to_string(), TermcapValue::Boolean(true));
89        self.capabilities
90            .insert("ut".to_string(), TermcapValue::Boolean(is_xterm));
91
92        let cols = std::env::var("COLUMNS")
93            .ok()
94            .and_then(|s| s.parse().ok())
95            .unwrap_or(80);
96        let lines = std::env::var("LINES")
97            .ok()
98            .and_then(|s| s.parse().ok())
99            .unwrap_or(24);
100        let colors = if term.contains("256") {
101            256
102        } else if is_xterm {
103            8
104        } else {
105            2
106        };
107
108        self.capabilities
109            .insert("co".to_string(), TermcapValue::Number(cols));
110        self.capabilities
111            .insert("li".to_string(), TermcapValue::Number(lines));
112        self.capabilities
113            .insert("Co".to_string(), TermcapValue::Number(colors));
114        self.capabilities
115            .insert("it".to_string(), TermcapValue::Number(8));
116
117        if is_ansi {
118            self.capabilities.insert(
119                "cl".to_string(),
120                TermcapValue::String("\x1b[H\x1b[2J".to_string()),
121            );
122            self.capabilities.insert(
123                "cm".to_string(),
124                TermcapValue::String("\x1b[%i%d;%dH".to_string()),
125            );
126            self.capabilities
127                .insert("up".to_string(), TermcapValue::String("\x1b[A".to_string()));
128            self.capabilities
129                .insert("do".to_string(), TermcapValue::String("\x1b[B".to_string()));
130            self.capabilities
131                .insert("nd".to_string(), TermcapValue::String("\x1b[C".to_string()));
132            self.capabilities
133                .insert("le".to_string(), TermcapValue::String("\x1b[D".to_string()));
134            self.capabilities
135                .insert("ho".to_string(), TermcapValue::String("\x1b[H".to_string()));
136            self.capabilities
137                .insert("ce".to_string(), TermcapValue::String("\x1b[K".to_string()));
138            self.capabilities
139                .insert("cd".to_string(), TermcapValue::String("\x1b[J".to_string()));
140            self.capabilities
141                .insert("me".to_string(), TermcapValue::String("\x1b[m".to_string()));
142            self.capabilities.insert(
143                "md".to_string(),
144                TermcapValue::String("\x1b[1m".to_string()),
145            );
146            self.capabilities.insert(
147                "mr".to_string(),
148                TermcapValue::String("\x1b[7m".to_string()),
149            );
150            self.capabilities.insert(
151                "us".to_string(),
152                TermcapValue::String("\x1b[4m".to_string()),
153            );
154            self.capabilities.insert(
155                "ue".to_string(),
156                TermcapValue::String("\x1b[24m".to_string()),
157            );
158            self.capabilities.insert(
159                "so".to_string(),
160                TermcapValue::String("\x1b[7m".to_string()),
161            );
162            self.capabilities.insert(
163                "se".to_string(),
164                TermcapValue::String("\x1b[27m".to_string()),
165            );
166            self.capabilities.insert(
167                "vi".to_string(),
168                TermcapValue::String("\x1b[?25l".to_string()),
169            );
170            self.capabilities.insert(
171                "ve".to_string(),
172                TermcapValue::String("\x1b[?25h".to_string()),
173            );
174            self.capabilities.insert(
175                "ti".to_string(),
176                TermcapValue::String("\x1b[?1049h".to_string()),
177            );
178            self.capabilities.insert(
179                "te".to_string(),
180                TermcapValue::String("\x1b[?1049l".to_string()),
181            );
182            self.capabilities
183                .insert("bl".to_string(), TermcapValue::String("\x07".to_string()));
184            self.capabilities
185                .insert("cr".to_string(), TermcapValue::String("\r".to_string()));
186        }
187    }
188
189    /// Get a boolean capability
190    pub fn get_flag(&self, name: &str) -> Option<bool> {
191        match self.capabilities.get(name)? {
192            TermcapValue::Boolean(b) => Some(*b),
193            _ => None,
194        }
195    }
196
197    /// Get a numeric capability
198    pub fn get_num(&self, name: &str) -> Option<i32> {
199        match self.capabilities.get(name)? {
200            TermcapValue::Number(n) => Some(*n),
201            _ => None,
202        }
203    }
204
205    /// Get a string capability
206    pub fn get_str(&self, name: &str) -> Option<String> {
207        match self.capabilities.get(name)? {
208            TermcapValue::String(s) => Some(s.clone()),
209            _ => None,
210        }
211    }
212
213    /// Get any capability
214    pub fn get(&self, name: &str) -> Option<&TermcapValue> {
215        self.capabilities.get(name)
216    }
217
218    /// Is termcap initialized?
219    pub fn is_initialized(&self) -> bool {
220        self.initialized
221    }
222
223    /// Get all boolean capabilities
224    pub fn booleans(&self) -> HashMap<String, bool> {
225        self.capabilities
226            .iter()
227            .filter_map(|(k, v)| {
228                if let TermcapValue::Boolean(b) = v {
229                    Some((k.clone(), *b))
230                } else {
231                    None
232                }
233            })
234            .collect()
235    }
236
237    /// Get all numeric capabilities
238    pub fn numbers(&self) -> HashMap<String, i32> {
239        self.capabilities
240            .iter()
241            .filter_map(|(k, v)| {
242                if let TermcapValue::Number(n) = v {
243                    Some((k.clone(), *n))
244                } else {
245                    None
246                }
247            })
248            .collect()
249    }
250
251    /// Get all string capabilities
252    pub fn strings(&self) -> HashMap<String, String> {
253        self.capabilities
254            .iter()
255            .filter_map(|(k, v)| {
256                if let TermcapValue::String(s) = v {
257                    Some((k.clone(), s.clone()))
258                } else {
259                    None
260                }
261            })
262            .collect()
263    }
264}
265
266/// Apply tgoto-style parameter substitution
267pub fn tgoto(cap: &str, col: i32, row: i32) -> String {
268    let mut result = String::new();
269    let mut chars = cap.chars().peekable();
270    let mut use_row = true;
271
272    while let Some(c) = chars.next() {
273        if c == '%' {
274            if let Some(&next) = chars.peek() {
275                chars.next();
276                match next {
277                    'd' => {
278                        let val = if use_row { row } else { col };
279                        result.push_str(&val.to_string());
280                        use_row = false;
281                    }
282                    '2' => {
283                        let val = if use_row { row } else { col };
284                        result.push_str(&format!("{:02}", val));
285                        use_row = false;
286                    }
287                    '3' => {
288                        let val = if use_row { row } else { col };
289                        result.push_str(&format!("{:03}", val));
290                        use_row = false;
291                    }
292                    '.' => {
293                        let val = if use_row { row } else { col };
294                        result.push((val as u8) as char);
295                        use_row = false;
296                    }
297                    '+' => {
298                        if let Some(offset) = chars.next() {
299                            let val = if use_row { row } else { col };
300                            result.push(((val + offset as i32) as u8) as char);
301                            use_row = false;
302                        }
303                    }
304                    'i' => {}
305                    '%' => {
306                        result.push('%');
307                    }
308                    _ => {
309                        result.push('%');
310                        result.push(next);
311                    }
312                }
313            }
314        } else {
315            result.push(c);
316        }
317    }
318
319    result
320}
321
322/// Execute echotc builtin
323pub fn builtin_echotc(args: &[&str], tc: &Termcap) -> (i32, String) {
324    if args.is_empty() {
325        return (1, "echotc: capability name required\n".to_string());
326    }
327
328    if !tc.is_initialized() {
329        return (1, "echotc: terminal not initialized\n".to_string());
330    }
331
332    let cap_name = args[0];
333
334    if let Some(n) = tc.get_num(cap_name) {
335        return (0, format!("{}\n", n));
336    }
337
338    if let Some(b) = tc.get_flag(cap_name) {
339        return (0, format!("{}\n", if b { "yes" } else { "no" }));
340    }
341
342    if let Some(s) = tc.get_str(cap_name) {
343        if args.len() == 1 {
344            return (0, s);
345        }
346
347        let mut required_args = 0;
348        for c in s.chars() {
349            if c == '%' {
350                required_args += 1;
351            }
352        }
353        required_args /= 2;
354
355        if args.len() - 1 != required_args {
356            if args.len() - 1 < required_args {
357                return (1, "echotc: not enough arguments\n".to_string());
358            } else {
359                return (1, "echotc: too many arguments\n".to_string());
360            }
361        }
362
363        if required_args >= 2 {
364            let row: i32 = args[1].parse().unwrap_or(0);
365            let col: i32 = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(row);
366            return (0, tgoto(&s, col, row));
367        }
368
369        return (0, s);
370    }
371
372    (1, format!("echotc: no such capability: {}\n", cap_name))
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378
379    #[test]
380    fn test_termcap_new() {
381        let tc = Termcap::new();
382        assert!(!tc.is_initialized());
383    }
384
385    #[test]
386    fn test_termcap_init() {
387        let mut tc = Termcap::new();
388        let result = tc.init(Some("xterm-256color"));
389        assert!(result);
390        assert!(tc.is_initialized());
391    }
392
393    #[test]
394    fn test_termcap_get_num() {
395        let mut tc = Termcap::new();
396        tc.init(Some("xterm"));
397
398        assert!(tc.get_num("co").is_some());
399        assert!(tc.get_num("li").is_some());
400    }
401
402    #[test]
403    fn test_termcap_get_flag() {
404        let mut tc = Termcap::new();
405        tc.init(Some("xterm"));
406
407        assert_eq!(tc.get_flag("am"), Some(true));
408    }
409
410    #[test]
411    fn test_termcap_get_str() {
412        let mut tc = Termcap::new();
413        tc.init(Some("xterm"));
414
415        assert!(tc.get_str("cl").is_some());
416        assert!(tc.get_str("cm").is_some());
417    }
418
419    #[test]
420    fn test_tgoto() {
421        let result = tgoto("\x1b[%d;%dH", 10, 5);
422        assert!(result.contains("5") && result.contains("10"));
423    }
424
425    #[test]
426    fn test_builtin_echotc_no_args() {
427        let tc = Termcap::new();
428        let (status, _) = builtin_echotc(&[], &tc);
429        assert_eq!(status, 1);
430    }
431
432    #[test]
433    fn test_builtin_echotc_not_initialized() {
434        let tc = Termcap::new();
435        let (status, output) = builtin_echotc(&["co"], &tc);
436        assert_eq!(status, 1);
437        assert!(output.contains("not initialized"));
438    }
439
440    #[test]
441    fn test_builtin_echotc_numeric() {
442        let mut tc = Termcap::new();
443        tc.init(Some("xterm"));
444        let (status, output) = builtin_echotc(&["co"], &tc);
445        assert_eq!(status, 0);
446        assert!(output.contains("80") || output.parse::<i32>().is_ok());
447    }
448
449    #[test]
450    fn test_builtin_echotc_boolean() {
451        let mut tc = Termcap::new();
452        tc.init(Some("xterm"));
453        let (status, output) = builtin_echotc(&["am"], &tc);
454        assert_eq!(status, 0);
455        assert!(output.contains("yes") || output.contains("no"));
456    }
457
458    #[test]
459    fn test_bool_codes() {
460        assert!(BOOL_CODES.contains(&"am"));
461        assert!(BOOL_CODES.contains(&"bw"));
462    }
463
464    #[test]
465    fn test_num_codes() {
466        assert!(NUM_CODES.contains(&"co"));
467        assert!(NUM_CODES.contains(&"li"));
468    }
469
470    #[test]
471    fn test_str_codes() {
472        assert!(STR_CODES.contains(&"cl"));
473        assert!(STR_CODES.contains(&"cm"));
474    }
475}