| //! 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 crate::color; |
| use crate::Attr; |
| use crate::Terminal; |
| |
| use parm::{expand, Param, Variables}; |
| use parser::compiled::{msys_terminfo, parse}; |
| use searcher::get_dbpath_for_term; |
| |
| /// A parsed terminfo database entry. |
| #[derive(Debug)] |
| pub struct TermInfo { |
| /// Names for the terminal |
| pub names: Vec<String>, |
| /// Map of capability name to boolean value |
| pub bools: HashMap<String, bool>, |
| /// Map of capability name to numeric value |
| pub numbers: HashMap<String, u32>, |
| /// Map of capability name to raw (unexpanded) string |
| pub strings: HashMap<String, Vec<u8>>, |
| } |
| |
| /// A terminfo creation error. |
| #[derive(Debug)] |
| pub 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 fn from_env() -> Result<TermInfo, Error> { |
| 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 fn from_name(name: &str) -> Result<TermInfo, Error> { |
| 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 fn from_path<P: AsRef<Path>>(path: P) -> Result<TermInfo, Error> { |
| Self::_from_path(path.as_ref()) |
| } |
| // Keep the metadata small |
| fn _from_path(path: &Path) -> Result<TermInfo, Error> { |
| let file = File::open(path).map_err(Error::IoError)?; |
| let mut reader = BufReader::new(file); |
| parse(&mut reader, false).map_err(Error::MalformedTerminfo) |
| } |
| } |
| |
| pub mod searcher; |
| |
| /// TermInfo format parsing. |
| pub mod parser { |
| //! ncurses-compatible compiled terminfo format parsing (term(5)) |
| pub mod compiled; |
| } |
| 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. |
| pub struct TerminfoTerminal<T> { |
| num_colors: u32, |
| out: T, |
| ti: TermInfo, |
| } |
| |
| impl<T: Write + Send> Terminal for TerminfoTerminal<T> { |
| type Output = T; |
| fn fg(&mut self, color: color::Color) -> io::Result<bool> { |
| 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 bg(&mut self, color: color::Color) -> io::Result<bool> { |
| let color = self.dim_if_necessary(color); |
| if self.num_colors > color { |
| return self.apply_cap("setab", &[Param::Number(color as i32)]); |
| } |
| Ok(false) |
| } |
| |
| fn attr(&mut self, attr: Attr) -> io::Result<bool> { |
| match attr { |
| Attr::ForegroundColor(c) => self.fg(c), |
| Attr::BackgroundColor(c) => self.bg(c), |
| _ => self.apply_cap(cap_for_attr(attr), &[]), |
| } |
| } |
| |
| 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) -> io::Result<bool> { |
| // 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)) |
| } |
| |
| 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 + Send> TerminfoTerminal<T> { |
| /// Creates 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, 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 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 } |
| } |
| |
| fn apply_cap(&mut self, cmd: &str, params: &[Param]) -> io::Result<bool> { |
| 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<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() |
| } |
| } |