//! Terminfo database interface. use std::collections::HashMap; use std::env; use std::error; use std::fmt; use std::fs::File; use std::io::{self, prelude::*, BufReader}; use std::path::Path; use super::color; use super::Terminal; use parm::{expand, Param, Variables}; use parser::compiled::{msys_terminfo, parse}; use searcher::get_dbpath_for_term; /// A parsed terminfo database entry. #[allow(unused)] #[derive(Debug)] pub(crate) struct TermInfo { /// Names for the terminal pub(crate) names: Vec, /// Map of capability name to boolean value pub(crate) bools: HashMap, /// Map of capability name to numeric value pub(crate) numbers: HashMap, /// Map of capability name to raw (unexpanded) string pub(crate) strings: HashMap>, } /// A terminfo creation error. #[derive(Debug)] pub(crate) enum Error { /// TermUnset Indicates that the environment doesn't include enough information to find /// the terminfo entry. TermUnset, /// MalformedTerminfo indicates that parsing the terminfo entry failed. MalformedTerminfo(String), /// io::Error forwards any io::Errors encountered when finding or reading the terminfo entry. IoError(io::Error), } impl error::Error for Error { fn source(&self) -> Option<&(dyn error::Error + 'static)> { use Error::*; match self { IoError(e) => Some(e), _ => None, } } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use Error::*; match *self { TermUnset => Ok(()), MalformedTerminfo(ref e) => e.fmt(f), IoError(ref e) => e.fmt(f), } } } impl TermInfo { /// Creates a TermInfo based on current environment. pub(crate) fn from_env() -> Result { let term = match env::var("TERM") { Ok(name) => TermInfo::from_name(&name), Err(..) => return Err(Error::TermUnset), }; if term.is_err() && env::var("MSYSCON").map_or(false, |s| "mintty.exe" == s) { // msys terminal Ok(msys_terminfo()) } else { term } } /// Creates a TermInfo for the named terminal. pub(crate) fn from_name(name: &str) -> Result { get_dbpath_for_term(name) .ok_or_else(|| { Error::IoError(io::Error::new(io::ErrorKind::NotFound, "terminfo file not found")) }) .and_then(|p| TermInfo::from_path(&(*p))) } /// Parse the given TermInfo. pub(crate) fn from_path>(path: P) -> Result { Self::_from_path(path.as_ref()) } // Keep the metadata small fn _from_path(path: &Path) -> Result { let file = File::open(path).map_err(Error::IoError)?; let mut reader = BufReader::new(file); parse(&mut reader, false).map_err(Error::MalformedTerminfo) } } pub(crate) mod searcher; /// TermInfo format parsing. pub(crate) mod parser { //! ncurses-compatible compiled terminfo format parsing (term(5)) pub(crate) mod compiled; } pub(crate) mod parm; /// A Terminal that knows how many colors it supports, with a reference to its /// parsed Terminfo database record. pub(crate) struct TerminfoTerminal { num_colors: u32, out: T, ti: TermInfo, } impl Terminal for TerminfoTerminal { fn fg(&mut self, color: color::Color) -> io::Result { let color = self.dim_if_necessary(color); if self.num_colors > color { return self.apply_cap("setaf", &[Param::Number(color as i32)]); } Ok(false) } fn reset(&mut self) -> io::Result { // are there any terminals that have color/attrs and not sgr0? // Try falling back to sgr, then op let cmd = match ["sgr0", "sgr", "op"].iter().find_map(|cap| self.ti.strings.get(*cap)) { Some(op) => match expand(&op, &[], &mut Variables::new()) { Ok(cmd) => cmd, Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)), }, None => return Ok(false), }; self.out.write_all(&cmd).and(Ok(true)) } } impl TerminfoTerminal { /// Creates a new TerminfoTerminal with the given TermInfo and Write. pub(crate) fn new_with_terminfo(out: T, terminfo: TermInfo) -> TerminfoTerminal { 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, ti: terminfo, num_colors: nc } } /// Creates a new TerminfoTerminal for the current environment with the given Write. /// /// Returns `None` when the terminfo cannot be found or parsed. pub(crate) fn new(out: T) -> Option> { 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 } } fn apply_cap(&mut self, cmd: &str, params: &[Param]) -> io::Result { match self.ti.strings.get(cmd) { Some(cmd) => match expand(&cmd, params, &mut Variables::new()) { Ok(s) => self.out.write_all(&s).and(Ok(true)), Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e)), }, None => Ok(false), } } } impl Write for TerminfoTerminal { fn write(&mut self, buf: &[u8]) -> io::Result { self.out.write(buf) } fn flush(&mut self) -> io::Result<()> { self.out.flush() } }