term/terminfo/
mod.rs

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) => 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) => write!(f, "bad magic number {:x} in terminfo header", v),
211            ShortNames => f.write_str("no names exposed, need at least one"),
212            TooManyBools => f.write_str("more boolean properties than libterm knows about"),
213            TooManyNumbers => f.write_str("more number properties than libterm knows about"),
214            TooManyStrings => f.write_str("more string properties than libterm knows about"),
215            InvalidLength => f.write_str("invalid length field value, must be >= -1"),
216            NotUtf8(ref e) => e.fmt(f),
217            NamesMissingNull => f.write_str("names table missing NUL terminator"),
218            StringsMissingNull => f.write_str("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) => 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}