blob: d9d17bc4c368d3ca04d0ae8254a68d0a13dd24a7 [file] [log] [blame]
// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! Terminfo database interface.
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::prelude::*;
use std::io;
use std::io::BufReader;
use std::path::Path;
use Attr;
use color;
use Terminal;
use Result;
use self::searcher::get_dbpath_for_term;
use self::parser::compiled::parse;
use self::parm::{expand, Param, Variables};
use self::Error::*;
/// Returns true if the named terminal supports basic ANSI escape codes.
fn is_ansi(name: &str) -> bool {
// SORTED! We binary search this.
static ANSI_TERM_PREFIX: &'static [&'static str] = &[
"Eterm", "ansi", "eterm", "iterm", "konsole", "linux", "mrxvt", "msyscon", "rxvt",
"screen", "tmux", "xterm",
];
match ANSI_TERM_PREFIX.binary_search(&name) {
Ok(_) => true,
Err(0) => false,
Err(idx) => name.starts_with(ANSI_TERM_PREFIX[idx - 1]),
}
}
/// A parsed terminfo database entry.
#[derive(Debug, Clone)]
pub struct TermInfo {
/// Names for the terminal
pub names: Vec<String>,
/// Map of capability name to boolean value
pub bools: HashMap<&'static str, bool>,
/// Map of capability name to numeric value
pub numbers: HashMap<&'static str, u32>,
/// Map of capability name to raw (unexpanded) string
pub strings: HashMap<&'static str, Vec<u8>>,
}
impl TermInfo {
/// Create a `TermInfo` based on current environment.
pub fn from_env() -> Result<TermInfo> {
let term_var = env::var("TERM").ok();
let term_name = term_var.as_ref().map(|s| &**s).or_else(|| {
env::var("MSYSCON").ok().and_then(|s| {
if s == "mintty.exe" {
Some("msyscon")
} else {
None
}
})
});
if let Some(term_name) = term_name {
return TermInfo::from_name(term_name);
} else {
return Err(::Error::TermUnset);
}
}
/// Create a `TermInfo` for the named terminal.
pub fn from_name(name: &str) -> Result<TermInfo> {
if let Some(path) = get_dbpath_for_term(name) {
match TermInfo::from_path(&path) {
Ok(term) => return Ok(term),
// Skip IO Errors (e.g., permission denied).
Err(::Error::Io(_)) => {}
// Don't ignore malformed terminfo databases.
Err(e) => return Err(e),
}
}
// Basic ANSI fallback terminal.
if is_ansi(name) {
let mut strings = HashMap::new();
strings.insert("sgr0", b"\x1B[0m".to_vec());
strings.insert("bold", b"\x1B[1m".to_vec());
strings.insert("setaf", b"\x1B[3%p1%dm".to_vec());
strings.insert("setab", b"\x1B[4%p1%dm".to_vec());
let mut numbers = HashMap::new();
numbers.insert("colors", 8);
Ok(TermInfo {
names: vec![name.to_owned()],
bools: HashMap::new(),
numbers: numbers,
strings: strings,
})
} else {
Err(::Error::TerminfoEntryNotFound)
}
}
/// Parse the given `TermInfo`.
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<TermInfo> {
Self::_from_path(path.as_ref())
}
// Keep the metadata small
// (That is, this uses a &Path so that this function need not be instantiated
// for every type
// which implements AsRef<Path>. One day, if/when rustc is a bit smarter, it
// might do this for
// us. Alas. )
fn _from_path(path: &Path) -> Result<TermInfo> {
let file = File::open(path).map_err(::Error::Io)?;
let mut reader = BufReader::new(file);
parse(&mut reader, false)
}
/// Retrieve a capability `cmd` and expand it with `params`, writing result to `out`.
pub fn apply_cap(&self, cmd: &str, params: &[Param], out: &mut io::Write) -> Result<()> {
match self.strings.get(cmd) {
Some(cmd) => match expand(cmd, params, &mut Variables::new()) {
Ok(s) => {
out.write_all(&s)?;
Ok(())
}
Err(e) => Err(e.into()),
},
None => Err(::Error::NotSupported),
}
}
/// Write the reset string to `out`.
pub fn reset(&self, out: &mut io::Write) -> Result<()> {
// are there any terminals that have color/attrs and not sgr0?
// Try falling back to sgr, then op
let cmd = match [
("sgr0", &[] as &[Param]),
("sgr", &[Param::Number(0)]),
("op", &[]),
].iter()
.filter_map(|&(cap, params)| self.strings.get(cap).map(|c| (c, params)))
.next()
{
Some((op, params)) => match expand(op, params, &mut Variables::new()) {
Ok(cmd) => cmd,
Err(e) => return Err(e.into()),
},
None => return Err(::Error::NotSupported),
};
out.write_all(&cmd)?;
Ok(())
}
}
#[derive(Debug, Eq, PartialEq)]
/// An error from parsing a terminfo entry
pub enum Error {
/// The "magic" number at the start of the file was wrong.
///
/// It should be `0x11A` (16bit numbers) or `0x21e` (32bit numbers)
BadMagic(u16),
/// The names in the file were not valid UTF-8.
///
/// In theory these should only be ASCII, but to work with the Rust `str` type, we treat them
/// as UTF-8. This is valid, except when a terminfo file decides to be invalid. This hasn't
/// been encountered in the wild.
NotUtf8(::std::str::Utf8Error),
/// The names section of the file was empty
ShortNames,
/// More boolean parameters are present in the file than this crate knows how to interpret.
TooManyBools,
/// More number parameters are present in the file than this crate knows how to interpret.
TooManyNumbers,
/// More string parameters are present in the file than this crate knows how to interpret.
TooManyStrings,
/// The length of some field was not >= -1.
InvalidLength,
/// The names table was missing a trailing null terminator.
NamesMissingNull,
/// The strings table was missing a trailing null terminator.
StringsMissingNull,
}
impl ::std::fmt::Display for Error {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
use std::error::Error;
match *self {
NotUtf8(e) => write!(f, "{}", e),
BadMagic(v) => write!(f, "bad magic number {:x} in terminfo header", v),
_ => f.write_str(self.description()),
}
}
}
impl ::std::convert::From<::std::string::FromUtf8Error> for Error {
fn from(v: ::std::string::FromUtf8Error) -> Self {
NotUtf8(v.utf8_error())
}
}
impl ::std::error::Error for Error {
fn description(&self) -> &str {
match *self {
BadMagic(..) => "incorrect magic number at start of file",
ShortNames => "no names exposed, need at least one",
TooManyBools => "more boolean properties than libterm knows about",
TooManyNumbers => "more number properties than libterm knows about",
TooManyStrings => "more string properties than libterm knows about",
InvalidLength => "invalid length field value, must be >= -1",
NotUtf8(ref e) => e.description(),
NamesMissingNull => "names table missing NUL terminator",
StringsMissingNull => "string table missing NUL terminator",
}
}
fn cause(&self) -> Option<&::std::error::Error> {
match *self {
NotUtf8(ref e) => Some(e),
_ => None,
}
}
}
pub mod searcher;
/// `TermInfo` format parsing.
pub mod parser {
//! ncurses-compatible compiled terminfo format parsing (term(5))
pub mod compiled;
mod names;
}
pub mod parm;
fn cap_for_attr(attr: Attr) -> &'static str {
match attr {
Attr::Bold => "bold",
Attr::Dim => "dim",
Attr::Italic(true) => "sitm",
Attr::Italic(false) => "ritm",
Attr::Underline(true) => "smul",
Attr::Underline(false) => "rmul",
Attr::Blink => "blink",
Attr::Standout(true) => "smso",
Attr::Standout(false) => "rmso",
Attr::Reverse => "rev",
Attr::Secure => "invis",
Attr::ForegroundColor(_) => "setaf",
Attr::BackgroundColor(_) => "setab",
}
}
/// A Terminal that knows how many colors it supports, with a reference to its
/// parsed Terminfo database record.
#[derive(Clone, Debug)]
pub struct TerminfoTerminal<T> {
num_colors: u32,
out: T,
ti: TermInfo,
}
impl<T: Write> Terminal for TerminfoTerminal<T> {
type Output = T;
fn fg(&mut self, color: color::Color) -> Result<()> {
let color = self.dim_if_necessary(color);
if self.num_colors > color {
return self.ti
.apply_cap("setaf", &[Param::Number(color as i32)], &mut self.out);
}
Err(::Error::ColorOutOfRange)
}
fn bg(&mut self, color: color::Color) -> Result<()> {
let color = self.dim_if_necessary(color);
if self.num_colors > color {
return self.ti
.apply_cap("setab", &[Param::Number(color as i32)], &mut self.out);
}
Err(::Error::ColorOutOfRange)
}
fn attr(&mut self, attr: Attr) -> Result<()> {
match attr {
Attr::ForegroundColor(c) => self.fg(c),
Attr::BackgroundColor(c) => self.bg(c),
_ => self.ti.apply_cap(cap_for_attr(attr), &[], &mut self.out),
}
}
fn supports_attr(&self, attr: Attr) -> bool {
match attr {
Attr::ForegroundColor(_) | Attr::BackgroundColor(_) => self.num_colors > 0,
_ => {
let cap = cap_for_attr(attr);
self.ti.strings.get(cap).is_some()
}
}
}
fn reset(&mut self) -> Result<()> {
self.ti.reset(&mut self.out)
}
fn supports_reset(&self) -> bool {
["sgr0", "sgr", "op"]
.iter()
.any(|&cap| self.ti.strings.get(cap).is_some())
}
fn supports_color(&self) -> bool {
self.num_colors > 0 && self.supports_reset()
}
fn cursor_up(&mut self) -> Result<()> {
self.ti.apply_cap("cuu1", &[], &mut self.out)
}
fn delete_line(&mut self) -> Result<()> {
self.ti.apply_cap("el", &[], &mut self.out)
}
fn carriage_return(&mut self) -> Result<()> {
self.ti.apply_cap("cr", &[], &mut self.out)
}
fn get_ref(&self) -> &T {
&self.out
}
fn get_mut(&mut self) -> &mut T {
&mut self.out
}
fn into_inner(self) -> T
where
Self: Sized,
{
self.out
}
}
impl<T: Write> TerminfoTerminal<T> {
/// Create a new TerminfoTerminal with the given TermInfo and Write.
pub fn new_with_terminfo(out: T, terminfo: TermInfo) -> TerminfoTerminal<T> {
let nc = if terminfo.strings.contains_key("setaf") && terminfo.strings.contains_key("setab")
{
terminfo.numbers.get("colors").map_or(0, |&n| n)
} else {
0
};
TerminfoTerminal {
out: out,
ti: terminfo,
num_colors: nc as u32,
}
}
/// Create a new TerminfoTerminal for the current environment with the given Write.
///
/// Returns `None` when the terminfo cannot be found or parsed.
pub fn new(out: T) -> Option<TerminfoTerminal<T>> {
TermInfo::from_env()
.map(move |ti| TerminfoTerminal::new_with_terminfo(out, ti))
.ok()
}
fn dim_if_necessary(&self, color: color::Color) -> color::Color {
if color >= self.num_colors && color >= 8 && color < 16 {
color - 8
} else {
color
}
}
}
impl<T: Write> Write for TerminfoTerminal<T> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.out.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.out.flush()
}
}