1use std::collections::HashMap;
6
7pub 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
14pub 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
21pub 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#[derive(Debug, Clone)]
39pub enum TermcapValue {
40 Boolean(bool),
41 Number(i32),
42 String(String),
43}
44
45#[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 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 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 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 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 pub fn get(&self, name: &str) -> Option<&TermcapValue> {
215 self.capabilities.get(name)
216 }
217
218 pub fn is_initialized(&self) -> bool {
220 self.initialized
221 }
222
223 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 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 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
266pub 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
322pub 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}