1use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
9pub enum TermCapability {
10 Boolean(bool),
11 Number(i32),
12 String(String),
13}
14
15#[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 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 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 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 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 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 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 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 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 pub fn is_initialized(&self) -> bool {
248 self.initialized
249 }
250
251 pub fn terminal(&self) -> Option<&str> {
253 self.terminal.as_deref()
254 }
255}
256
257pub 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
264pub 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
271pub 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
309pub 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}