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