Skip to main content

zsh/
terminfo.rs

1//! Terminfo module - port of Modules/terminfo.c
2//!
3//! Provides access to terminal capabilities via terminfo database.
4
5use std::collections::HashMap;
6
7/// Terminfo capability types
8#[derive(Debug, Clone)]
9pub enum TermCapability {
10    Boolean(bool),
11    Number(i32),
12    String(String),
13}
14
15/// Terminfo interface - using environment and basic capabilities
16#[derive(Debug, Default)]
17pub struct Terminfo {
18    initialized: bool,
19    terminal: Option<String>,
20    capabilities: HashMap<String, TermCapability>,
21}
22
23impl Terminfo {
24    pub fn new() -> Self {
25        Self::default()
26    }
27
28    /// Initialize terminfo for the given terminal
29    pub fn init(&mut self, term: Option<&str>) -> bool {
30        let terminal = term
31            .map(|s| s.to_string())
32            .or_else(|| std::env::var("TERM").ok());
33
34        if let Some(t) = terminal {
35            self.terminal = Some(t.clone());
36            self.load_basic_capabilities(&t);
37            self.initialized = true;
38            return true;
39        }
40
41        false
42    }
43
44    fn load_basic_capabilities(&mut self, term: &str) {
45        let is_xterm = term.contains("xterm") || term.contains("256color");
46        let _is_vt100 = term.contains("vt100") || term.contains("vt220");
47
48        self.capabilities
49            .insert("am".to_string(), TermCapability::Boolean(true));
50        self.capabilities
51            .insert("bce".to_string(), TermCapability::Boolean(is_xterm));
52        self.capabilities
53            .insert("km".to_string(), TermCapability::Boolean(true));
54        self.capabilities
55            .insert("mir".to_string(), TermCapability::Boolean(true));
56        self.capabilities
57            .insert("msgr".to_string(), TermCapability::Boolean(true));
58        self.capabilities
59            .insert("xenl".to_string(), TermCapability::Boolean(true));
60
61        let cols = std::env::var("COLUMNS")
62            .ok()
63            .and_then(|s| s.parse().ok())
64            .unwrap_or(80);
65        let lines = std::env::var("LINES")
66            .ok()
67            .and_then(|s| s.parse().ok())
68            .unwrap_or(24);
69        let colors = if is_xterm && term.contains("256") {
70            256
71        } else if is_xterm {
72            8
73        } else {
74            2
75        };
76
77        self.capabilities
78            .insert("cols".to_string(), TermCapability::Number(cols));
79        self.capabilities
80            .insert("lines".to_string(), TermCapability::Number(lines));
81        self.capabilities
82            .insert("colors".to_string(), TermCapability::Number(colors));
83        self.capabilities
84            .insert("it".to_string(), TermCapability::Number(8));
85
86        self.capabilities.insert(
87            "clear".to_string(),
88            TermCapability::String("\x1b[H\x1b[2J".to_string()),
89        );
90        self.capabilities.insert(
91            "cup".to_string(),
92            TermCapability::String("\x1b[%i%p1%d;%p2%dH".to_string()),
93        );
94        self.capabilities.insert(
95            "cuu1".to_string(),
96            TermCapability::String("\x1b[A".to_string()),
97        );
98        self.capabilities.insert(
99            "cud1".to_string(),
100            TermCapability::String("\x1b[B".to_string()),
101        );
102        self.capabilities.insert(
103            "cuf1".to_string(),
104            TermCapability::String("\x1b[C".to_string()),
105        );
106        self.capabilities.insert(
107            "cub1".to_string(),
108            TermCapability::String("\x1b[D".to_string()),
109        );
110        self.capabilities.insert(
111            "home".to_string(),
112            TermCapability::String("\x1b[H".to_string()),
113        );
114        self.capabilities.insert(
115            "el".to_string(),
116            TermCapability::String("\x1b[K".to_string()),
117        );
118        self.capabilities.insert(
119            "ed".to_string(),
120            TermCapability::String("\x1b[J".to_string()),
121        );
122        self.capabilities.insert(
123            "sgr0".to_string(),
124            TermCapability::String("\x1b[m".to_string()),
125        );
126        self.capabilities.insert(
127            "bold".to_string(),
128            TermCapability::String("\x1b[1m".to_string()),
129        );
130        self.capabilities.insert(
131            "rev".to_string(),
132            TermCapability::String("\x1b[7m".to_string()),
133        );
134        self.capabilities.insert(
135            "smul".to_string(),
136            TermCapability::String("\x1b[4m".to_string()),
137        );
138        self.capabilities.insert(
139            "rmul".to_string(),
140            TermCapability::String("\x1b[24m".to_string()),
141        );
142        self.capabilities.insert(
143            "smso".to_string(),
144            TermCapability::String("\x1b[7m".to_string()),
145        );
146        self.capabilities.insert(
147            "rmso".to_string(),
148            TermCapability::String("\x1b[27m".to_string()),
149        );
150
151        if is_xterm {
152            self.capabilities.insert(
153                "setaf".to_string(),
154                TermCapability::String("\x1b[3%p1%dm".to_string()),
155            );
156            self.capabilities.insert(
157                "setab".to_string(),
158                TermCapability::String("\x1b[4%p1%dm".to_string()),
159            );
160        }
161    }
162
163    /// Get a boolean capability
164    pub fn get_flag(&self, name: &str) -> Option<bool> {
165        if !self.initialized {
166            return None;
167        }
168
169        match self.capabilities.get(name)? {
170            TermCapability::Boolean(b) => Some(*b),
171            _ => None,
172        }
173    }
174
175    /// Get a numeric capability
176    pub fn get_num(&self, name: &str) -> Option<i32> {
177        if !self.initialized {
178            return None;
179        }
180
181        match self.capabilities.get(name)? {
182            TermCapability::Number(n) => Some(*n),
183            _ => None,
184        }
185    }
186
187    /// Get a string capability
188    pub fn get_str(&self, name: &str) -> Option<String> {
189        if !self.initialized {
190            return None;
191        }
192
193        match self.capabilities.get(name)? {
194            TermCapability::String(s) => Some(s.clone()),
195            _ => None,
196        }
197    }
198
199    /// Get any capability (auto-detect type)
200    pub fn get(&self, name: &str) -> Option<TermCapability> {
201        if let Some(n) = self.get_num(name) {
202            return Some(TermCapability::Number(n));
203        }
204        if let Some(b) = self.get_flag(name) {
205            return Some(TermCapability::Boolean(b));
206        }
207        if let Some(s) = self.get_str(name) {
208            return Some(TermCapability::String(s));
209        }
210        None
211    }
212
213    /// Get all boolean capabilities
214    pub fn booleans(&self) -> HashMap<String, bool> {
215        let mut result = HashMap::new();
216        for name in BOOL_NAMES.iter() {
217            if let Some(val) = self.get_flag(name) {
218                result.insert(name.to_string(), val);
219            }
220        }
221        result
222    }
223
224    /// Get all numeric capabilities
225    pub fn numbers(&self) -> HashMap<String, i32> {
226        let mut result = HashMap::new();
227        for name in NUM_NAMES.iter() {
228            if let Some(val) = self.get_num(name) {
229                result.insert(name.to_string(), val);
230            }
231        }
232        result
233    }
234
235    /// Get all string capabilities
236    pub fn strings(&self) -> HashMap<String, String> {
237        let mut result = HashMap::new();
238        for name in STR_NAMES.iter() {
239            if let Some(val) = self.get_str(name) {
240                result.insert(name.to_string(), val);
241            }
242        }
243        result
244    }
245
246    /// Is terminfo initialized?
247    pub fn is_initialized(&self) -> bool {
248        self.initialized
249    }
250
251    /// Get current terminal name
252    pub fn terminal(&self) -> Option<&str> {
253        self.terminal.as_deref()
254    }
255}
256
257/// Boolean capability names
258pub static BOOL_NAMES: &[&str] = &[
259    "bw", "am", "bce", "ccc", "xhp", "xhpa", "cpix", "crxm", "xt", "xenl", "eo", "gn", "hc",
260    "chts", "km", "daisy", "hs", "hls", "in", "lpix", "da", "db", "mir", "msgr", "nxon", "xsb",
261    "npc", "ndscr", "nrrmc", "os", "mc5i", "xvpa", "sam", "eslok", "hz", "ul", "xon",
262];
263
264/// Numeric capability names
265pub static NUM_NAMES: &[&str] = &[
266    "cols", "it", "lh", "lw", "lines", "lm", "xmc", "ma", "colors", "pairs", "wnum", "ncv", "nlab",
267    "pb", "vt", "wsl", "bitwin", "bitype", "bufsz", "btns", "spinh", "spinv", "maddr", "mjump",
268    "mcs", "mls", "npins", "orc", "orhi", "orl", "orvi", "cps", "widcs",
269];
270
271/// String capability names
272pub static STR_NAMES: &[&str] = &[
273    "acsc", "cbt", "bel", "cr", "cpi", "lpi", "chr", "cvr", "csr", "rmp", "tbc", "mgc", "clear",
274    "el1", "el", "ed", "hpa", "cmdch", "cwin", "cup", "cud1", "home", "civis", "cub1", "mrcup",
275    "cnorm", "cuf1", "ll", "cuu1", "cvvis", "defc", "dch1", "dl1", "dial", "dsl", "dclk", "hd",
276    "enacs", "smacs", "smam", "blink", "bold", "smcup", "smdc", "dim", "swidm", "sdrfq", "smir",
277    "sitm", "slm", "smicm", "snlq", "snrmq", "prot", "rev", "invis", "sshm", "smso", "ssubm",
278    "ssupm", "smul", "sum", "smxon", "ech", "rmacs", "rmam", "sgr0", "rmcup", "rmdc", "rwidm",
279    "rmir", "ritm", "rlm", "rmicm", "rshm", "rmso", "rsubm", "rsupm", "rmul", "rum", "rmxon",
280    "pause", "hook", "flash", "ff", "fsl", "wingo", "hup", "is1", "is2", "is3", "if", "iprog",
281    "initc", "initp", "ich1", "il1", "ip", "ka1", "ka3", "kb2", "kbs", "kbeg", "kcbt", "kc1",
282    "kc3", "kcan", "ktbc", "kclr", "kclo", "kcmd", "kcpy", "kcrt", "kctab", "kdch1", "kdl1",
283    "kcud1", "krmir", "kend", "kent", "kel", "ked", "kext", "kf0", "kf1", "kf10", "kf11", "kf12",
284    "kf13", "kf14", "kf15", "kf16", "kf17", "kf18", "kf19", "kf2", "kf20", "kf21", "kf22", "kf23",
285    "kf24", "kf25", "kf26", "kf27", "kf28", "kf29", "kf3", "kf30", "kf31", "kf32", "kf33", "kf34",
286    "kf35", "kf36", "kf37", "kf38", "kf39", "kf4", "kf40", "kf41", "kf42", "kf43", "kf44", "kf45",
287    "kf46", "kf47", "kf48", "kf49", "kf5", "kf50", "kf51", "kf52", "kf53", "kf54", "kf55", "kf56",
288    "kf57", "kf58", "kf59", "kf6", "kf60", "kf61", "kf62", "kf63", "kf7", "kf8", "kf9", "kfnd",
289    "khlp", "khome", "kich1", "kil1", "kcub1", "kll", "kmrk", "kmsg", "kmov", "knxt", "knp",
290    "kopn", "kopt", "kpp", "kprv", "kprt", "krdo", "kref", "krfr", "krpl", "krst", "kres", "kcuf1",
291    "ksav", "kBEG", "kCAN", "kCMD", "kCPY", "kCRT", "kDC", "kDL", "kslt", "kEND", "kEOL", "kEXT",
292    "kind", "kFND", "kHLP", "kHOM", "kIC", "kLFT", "kMSG", "kMOV", "kNXT", "kOPT", "kPRV", "kPRT",
293    "kri", "kRDO", "kRPL", "kRIT", "kRES", "kSAV", "kSPD", "khts", "kUND", "kspd", "kund", "kcuu1",
294    "rmkx", "smkx", "lf0", "lf1", "lf10", "lf2", "lf3", "lf4", "lf5", "lf6", "lf7", "lf8", "lf9",
295    "fln", "rmln", "smln", "rmm", "smm", "mhpa", "mcud1", "mcub1", "mcuf1", "mvpa", "mcuu1", "nel",
296    "porder", "oc", "op", "pad", "dch", "dl", "cud", "mcud", "ich", "indn", "il", "cub", "mcub",
297    "cuf", "mcuf", "rin", "cuu", "mcuu", "pfkey", "pfloc", "pfx", "pln", "mc0", "mc5p", "mc4",
298    "mc5", "pulse", "qdial", "rmclk", "rep", "rfi", "rs1", "rs2", "rs3", "rf", "rc", "vpa", "sc",
299    "ind", "ri", "scs", "sgr", "setb", "smgb", "smgbp", "sclk", "scp", "setf", "smgl", "smglp",
300    "smgr", "smgrp", "hts", "smgt", "smgtp", "wind", "sbim", "scsd", "rbim", "rcsd", "subcs",
301    "supcs", "ht", "docr", "tsl", "tone", "uc", "hu", "u0", "u1", "u2", "u3", "u4", "u5", "u6",
302    "u7", "u8", "u9", "wait", "xoffc", "xonc", "zerom", "scesa", "bicr", "binel", "birep", "csnm",
303    "csin", "colornm", "defbi", "devt", "dispc", "endbi", "smpch", "smsc", "rmpch", "rmsc", "getm",
304    "kmous", "minfo", "pctrm", "pfxl", "reqmp", "scesc", "s0ds", "s1ds", "s2ds", "s3ds", "setab",
305    "setaf", "setcolor", "smglr", "slines", "smgtb", "ehhlm", "elhlm", "elohlm", "erhlm", "ethlm",
306    "evhlm", "sgr1", "slength",
307];
308
309/// Execute echoti builtin
310pub fn builtin_echoti(args: &[&str]) -> (i32, String) {
311    if args.is_empty() {
312        return (1, "echoti: capability name required\n".to_string());
313    }
314
315    let cap_name = args[0];
316    let mut ti = Terminfo::new();
317
318    if !ti.init(None) {
319        return (1, "echoti: terminal not initialized\n".to_string());
320    }
321
322    if let Some(n) = ti.get_num(cap_name) {
323        return (0, format!("{}\n", n));
324    }
325
326    if let Some(b) = ti.get_flag(cap_name) {
327        return (0, format!("{}\n", if b { "yes" } else { "no" }));
328    }
329
330    if let Some(s) = ti.get_str(cap_name) {
331        if args.len() == 1 {
332            return (0, s);
333        }
334    }
335
336    (
337        1,
338        format!("echoti: no such terminfo capability: {}\n", cap_name),
339    )
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn test_terminfo_new() {
348        let ti = Terminfo::new();
349        assert!(!ti.is_initialized());
350    }
351
352    #[test]
353    fn test_term_capability_types() {
354        let b = TermCapability::Boolean(true);
355        let n = TermCapability::Number(80);
356        let s = TermCapability::String("test".to_string());
357
358        matches!(b, TermCapability::Boolean(true));
359        matches!(n, TermCapability::Number(80));
360        matches!(s, TermCapability::String(_));
361    }
362
363    #[test]
364    fn test_bool_names() {
365        assert!(BOOL_NAMES.contains(&"am"));
366        assert!(BOOL_NAMES.contains(&"bw"));
367    }
368
369    #[test]
370    fn test_num_names() {
371        assert!(NUM_NAMES.contains(&"cols"));
372        assert!(NUM_NAMES.contains(&"lines"));
373        assert!(NUM_NAMES.contains(&"colors"));
374    }
375
376    #[test]
377    fn test_str_names() {
378        assert!(STR_NAMES.contains(&"clear"));
379        assert!(STR_NAMES.contains(&"cup"));
380        assert!(STR_NAMES.contains(&"sgr0"));
381    }
382
383    #[test]
384    fn test_builtin_echoti_no_args() {
385        let (status, _) = builtin_echoti(&[]);
386        assert_eq!(status, 1);
387    }
388}