1// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
2// file at the top-level directory of this distribution and at
3// http://rust-lang.org/COPYRIGHT.
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8// option. This file may not be copied, modified, or distributed
9// except according to those terms.
10
11//! Terminfo database interface.
12
13use std::collections::HashMap;
14use std::env;
15use std::fs::File;
16use std::io;
17use std::io::prelude::*;
18use std::io::BufReader;
19use std::path::Path;
20
21#[cfg(windows)]
22use crate::win;
23
24use self::parm::{expand, Param, Variables};
25use self::parser::compiled::parse;
26use self::searcher::get_dbpath_for_term;
27use self::Error::*;
28use crate::color;
29use crate::Attr;
30use crate::Result;
31use crate::Terminal;
32
33/// Returns true if the named terminal supports basic ANSI escape codes.
34fn is_ansi(name: &str) -> bool {
35 // SORTED! We binary search this.
36 static ANSI_TERM_PREFIX: &[&str] = &[
37 "Eterm", "ansi", "eterm", "iterm", "konsole", "linux", "mrxvt", "msyscon", "rxvt",
38 "screen", "tmux", "xterm",
39 ];
40 match ANSI_TERM_PREFIX.binary_search(&name) {
41 Ok(_) => true,
42 Err(0) => false,
43 Err(idx: usize) => name.starts_with(ANSI_TERM_PREFIX[idx - 1]),
44 }
45}
46
47/// A parsed terminfo database entry.
48#[derive(Debug, Clone)]
49pub struct TermInfo {
50 /// Names for the terminal
51 pub names: Vec<String>,
52 /// Map of capability name to boolean value
53 pub bools: HashMap<&'static str, bool>,
54 /// Map of capability name to numeric value
55 pub numbers: HashMap<&'static str, u32>,
56 /// Map of capability name to raw (unexpanded) string
57 pub strings: HashMap<&'static str, Vec<u8>>,
58}
59
60impl TermInfo {
61 /// Create a `TermInfo` based on current environment.
62 pub fn from_env() -> Result<TermInfo> {
63 let term_var = env::var("TERM").ok();
64 let term_name = term_var.as_ref().map(|s| &**s).or_else(|| {
65 env::var("MSYSCON").ok().and_then(|s| {
66 if s == "mintty.exe" {
67 Some("msyscon")
68 } else {
69 None
70 }
71 })
72 });
73
74 #[cfg(windows)]
75 {
76 if term_name.is_none() && win::supports_ansi() {
77 // Microsoft people seem to be fine with pretending to be xterm:
78 // https://github.com/Microsoft/WSL/issues/1446
79 // The basic ANSI fallback terminal will be uses.
80 return TermInfo::from_name("xterm");
81 }
82 }
83
84 if let Some(term_name) = term_name {
85 TermInfo::from_name(term_name)
86 } else {
87 Err(crate::Error::TermUnset)
88 }
89 }
90
91 /// Create a `TermInfo` for the named terminal.
92 pub fn from_name(name: &str) -> Result<TermInfo> {
93 if let Some(path) = get_dbpath_for_term(name) {
94 match TermInfo::from_path(&path) {
95 Ok(term) => return Ok(term),
96 // Skip IO Errors (e.g., permission denied).
97 Err(crate::Error::Io(_)) => {}
98 // Don't ignore malformed terminfo databases.
99 Err(e) => return Err(e),
100 }
101 }
102 // Basic ANSI fallback terminal.
103 if is_ansi(name) {
104 let mut strings = HashMap::new();
105 strings.insert("sgr0", b"\x1B[0m".to_vec());
106 strings.insert("bold", b"\x1B[1m".to_vec());
107 strings.insert("setaf", b"\x1B[3%p1%dm".to_vec());
108 strings.insert("setab", b"\x1B[4%p1%dm".to_vec());
109
110 let mut numbers = HashMap::new();
111 numbers.insert("colors", 8);
112
113 Ok(TermInfo {
114 names: vec![name.to_owned()],
115 bools: HashMap::new(),
116 numbers: numbers,
117 strings: strings,
118 })
119 } else {
120 Err(crate::Error::TerminfoEntryNotFound)
121 }
122 }
123
124 /// Parse the given `TermInfo`.
125 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<TermInfo> {
126 Self::_from_path(path.as_ref())
127 }
128 // Keep the metadata small
129 // (That is, this uses a &Path so that this function need not be instantiated
130 // for every type
131 // which implements AsRef<Path>. One day, if/when rustc is a bit smarter, it
132 // might do this for
133 // us. Alas. )
134 fn _from_path(path: &Path) -> Result<TermInfo> {
135 let file = File::open(path).map_err(crate::Error::Io)?;
136 let mut reader = BufReader::new(file);
137 parse(&mut reader, false)
138 }
139
140 /// Retrieve a capability `cmd` and expand it with `params`, writing result to `out`.
141 pub fn apply_cap(&self, cmd: &str, params: &[Param], out: &mut dyn io::Write) -> Result<()> {
142 match self.strings.get(cmd) {
143 Some(cmd) => match expand(cmd, params, &mut Variables::new()) {
144 Ok(s) => {
145 out.write_all(&s)?;
146 Ok(())
147 }
148 Err(e) => Err(e.into()),
149 },
150 None => Err(crate::Error::NotSupported),
151 }
152 }
153
154 /// Write the reset string to `out`.
155 pub fn reset(&self, out: &mut dyn io::Write) -> Result<()> {
156 // are there any terminals that have color/attrs and not sgr0?
157 // Try falling back to sgr, then op
158 let cmd = match [
159 ("sgr0", &[] as &[Param]),
160 ("sgr", &[Param::Number(0)]),
161 ("op", &[]),
162 ]
163 .iter()
164 .filter_map(|&(cap, params)| self.strings.get(cap).map(|c| (c, params)))
165 .next()
166 {
167 Some((op, params)) => match expand(op, params, &mut Variables::new()) {
168 Ok(cmd) => cmd,
169 Err(e) => return Err(e.into()),
170 },
171 None => return Err(crate::Error::NotSupported),
172 };
173 out.write_all(&cmd)?;
174 Ok(())
175 }
176}
177
178#[derive(Debug, Eq, PartialEq)]
179/// An error from parsing a terminfo entry
180pub enum Error {
181 /// The "magic" number at the start of the file was wrong.
182 ///
183 /// It should be `0x11A` (16bit numbers) or `0x21e` (32bit numbers)
184 BadMagic(u16),
185 /// The names in the file were not valid UTF-8.
186 ///
187 /// In theory these should only be ASCII, but to work with the Rust `str` type, we treat them
188 /// as UTF-8. This is valid, except when a terminfo file decides to be invalid. This hasn't
189 /// been encountered in the wild.
190 NotUtf8(::std::str::Utf8Error),
191 /// The names section of the file was empty
192 ShortNames,
193 /// More boolean parameters are present in the file than this crate knows how to interpret.
194 TooManyBools,
195 /// More number parameters are present in the file than this crate knows how to interpret.
196 TooManyNumbers,
197 /// More string parameters are present in the file than this crate knows how to interpret.
198 TooManyStrings,
199 /// The length of some field was not >= -1.
200 InvalidLength,
201 /// The names table was missing a trailing null terminator.
202 NamesMissingNull,
203 /// The strings table was missing a trailing null terminator.
204 StringsMissingNull,
205}
206
207impl ::std::fmt::Display for Error {
208 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
209 match *self {
210 BadMagic(v: u16) => write!(f, "bad magic number {:x} in terminfo header", v),
211 ShortNames => f.write_str(data:"no names exposed, need at least one"),
212 TooManyBools => f.write_str(data:"more boolean properties than libterm knows about"),
213 TooManyNumbers => f.write_str(data:"more number properties than libterm knows about"),
214 TooManyStrings => f.write_str(data:"more string properties than libterm knows about"),
215 InvalidLength => f.write_str(data:"invalid length field value, must be >= -1"),
216 NotUtf8(ref e: &Utf8Error) => e.fmt(f),
217 NamesMissingNull => f.write_str(data:"names table missing NUL terminator"),
218 StringsMissingNull => f.write_str(data:"string table missing NUL terminator"),
219 }
220 }
221}
222
223impl ::std::convert::From<::std::string::FromUtf8Error> for Error {
224 fn from(v: ::std::string::FromUtf8Error) -> Self {
225 NotUtf8(v.utf8_error())
226 }
227}
228
229impl ::std::error::Error for Error {
230 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
231 match *self {
232 NotUtf8(ref e: &Utf8Error) => Some(e),
233 _ => None,
234 }
235 }
236}
237
238pub mod searcher;
239
240/// `TermInfo` format parsing.
241pub mod parser {
242 //! ncurses-compatible compiled terminfo format parsing (term(5))
243 pub mod compiled;
244 mod names;
245}
246pub mod parm;
247
248fn cap_for_attr(attr: Attr) -> &'static str {
249 match attr {
250 Attr::Bold => "bold",
251 Attr::Dim => "dim",
252 Attr::Italic(true) => "sitm",
253 Attr::Italic(false) => "ritm",
254 Attr::Underline(true) => "smul",
255 Attr::Underline(false) => "rmul",
256 Attr::Blink => "blink",
257 Attr::Standout(true) => "smso",
258 Attr::Standout(false) => "rmso",
259 Attr::Reverse => "rev",
260 Attr::Secure => "invis",
261 Attr::ForegroundColor(_) => "setaf",
262 Attr::BackgroundColor(_) => "setab",
263 }
264}
265
266/// A Terminal that knows how many colors it supports, with a reference to its
267/// parsed Terminfo database record.
268#[derive(Clone, Debug)]
269pub struct TerminfoTerminal<T> {
270 num_colors: u32,
271 out: T,
272 ti: TermInfo,
273}
274
275impl<T: Write> Terminal for TerminfoTerminal<T> {
276 type Output = T;
277 fn fg(&mut self, color: color::Color) -> Result<()> {
278 let color = self.dim_if_necessary(color);
279 if self.num_colors > color {
280 return self
281 .ti
282 .apply_cap("setaf", &[Param::Number(color as i32)], &mut self.out);
283 }
284 Err(crate::Error::ColorOutOfRange)
285 }
286
287 fn bg(&mut self, color: color::Color) -> Result<()> {
288 let color = self.dim_if_necessary(color);
289 if self.num_colors > color {
290 return self
291 .ti
292 .apply_cap("setab", &[Param::Number(color as i32)], &mut self.out);
293 }
294 Err(crate::Error::ColorOutOfRange)
295 }
296
297 fn attr(&mut self, attr: Attr) -> Result<()> {
298 match attr {
299 Attr::ForegroundColor(c) => self.fg(c),
300 Attr::BackgroundColor(c) => self.bg(c),
301 _ => self.ti.apply_cap(cap_for_attr(attr), &[], &mut self.out),
302 }
303 }
304
305 fn supports_attr(&self, attr: Attr) -> bool {
306 match attr {
307 Attr::ForegroundColor(_) | Attr::BackgroundColor(_) => self.num_colors > 0,
308 _ => {
309 let cap = cap_for_attr(attr);
310 self.ti.strings.get(cap).is_some()
311 }
312 }
313 }
314
315 fn reset(&mut self) -> Result<()> {
316 self.ti.reset(&mut self.out)
317 }
318
319 fn supports_reset(&self) -> bool {
320 ["sgr0", "sgr", "op"]
321 .iter()
322 .any(|&cap| self.ti.strings.get(cap).is_some())
323 }
324
325 fn supports_color(&self) -> bool {
326 self.num_colors > 0 && self.supports_reset()
327 }
328
329 fn cursor_up(&mut self) -> Result<()> {
330 self.ti.apply_cap("cuu1", &[], &mut self.out)
331 }
332
333 fn delete_line(&mut self) -> Result<()> {
334 self.ti.apply_cap("el", &[], &mut self.out)
335 }
336
337 fn carriage_return(&mut self) -> Result<()> {
338 self.ti.apply_cap("cr", &[], &mut self.out)
339 }
340
341 fn get_ref(&self) -> &T {
342 &self.out
343 }
344
345 fn get_mut(&mut self) -> &mut T {
346 &mut self.out
347 }
348
349 fn into_inner(self) -> T
350 where
351 Self: Sized,
352 {
353 self.out
354 }
355}
356
357impl<T: Write> TerminfoTerminal<T> {
358 /// Create a new TerminfoTerminal with the given TermInfo and Write.
359 pub fn new_with_terminfo(out: T, terminfo: TermInfo) -> TerminfoTerminal<T> {
360 let nc = if terminfo.strings.contains_key("setaf") && terminfo.strings.contains_key("setab")
361 {
362 terminfo.numbers.get("colors").map_or(0, |&n| n)
363 } else {
364 0
365 };
366
367 TerminfoTerminal {
368 out: out,
369 ti: terminfo,
370 num_colors: nc as u32,
371 }
372 }
373
374 /// Create a new TerminfoTerminal for the current environment with the given Write.
375 ///
376 /// Returns `None` when the terminfo cannot be found or parsed.
377 pub fn new(out: T) -> Option<TerminfoTerminal<T>> {
378 TermInfo::from_env()
379 .map(move |ti| TerminfoTerminal::new_with_terminfo(out, ti))
380 .ok()
381 }
382
383 fn dim_if_necessary(&self, color: color::Color) -> color::Color {
384 if color >= self.num_colors && color >= 8 && color < 16 {
385 color - 8
386 } else {
387 color
388 }
389 }
390}
391
392impl<T: Write> Write for TerminfoTerminal<T> {
393 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
394 self.out.write(buf)
395 }
396
397 fn flush(&mut self) -> io::Result<()> {
398 self.out.flush()
399 }
400}
401