| 1 | //! Terminfo database interface. |
| 2 | |
| 3 | use std::collections::HashMap; |
| 4 | use std::fs::File; |
| 5 | use std::io::prelude::*; |
| 6 | use std::path::Path; |
| 7 | use std::{env, error, fmt, io}; |
| 8 | |
| 9 | use parm::{Param, Variables, expand}; |
| 10 | use parser::compiled::{msys_terminfo, parse}; |
| 11 | use searcher::get_dbpath_for_term; |
| 12 | |
| 13 | use super::{Terminal, color}; |
| 14 | |
| 15 | /// A parsed terminfo database entry. |
| 16 | #[allow (unused)] |
| 17 | #[derive (Debug)] |
| 18 | pub(crate) struct TermInfo { |
| 19 | /// Names for the terminal |
| 20 | pub(crate) names: Vec<String>, |
| 21 | /// Map of capability name to boolean value |
| 22 | pub(crate) bools: HashMap<String, bool>, |
| 23 | /// Map of capability name to numeric value |
| 24 | pub(crate) numbers: HashMap<String, u32>, |
| 25 | /// Map of capability name to raw (unexpanded) string |
| 26 | pub(crate) strings: HashMap<String, Vec<u8>>, |
| 27 | } |
| 28 | |
| 29 | /// A terminfo creation error. |
| 30 | #[derive (Debug)] |
| 31 | pub(crate) enum Error { |
| 32 | /// TermUnset Indicates that the environment doesn't include enough information to find |
| 33 | /// the terminfo entry. |
| 34 | TermUnset, |
| 35 | /// MalformedTerminfo indicates that parsing the terminfo entry failed. |
| 36 | MalformedTerminfo(String), |
| 37 | /// io::Error forwards any io::Errors encountered when finding or reading the terminfo entry. |
| 38 | IoError(io::Error), |
| 39 | } |
| 40 | |
| 41 | impl error::Error for Error { |
| 42 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { |
| 43 | use Error::*; |
| 44 | match self { |
| 45 | IoError(e: &Error) => Some(e), |
| 46 | _ => None, |
| 47 | } |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | impl fmt::Display for Error { |
| 52 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 53 | use Error::*; |
| 54 | match *self { |
| 55 | TermUnset => Ok(()), |
| 56 | MalformedTerminfo(ref e: &String) => e.fmt(f), |
| 57 | IoError(ref e: &Error) => e.fmt(f), |
| 58 | } |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | impl TermInfo { |
| 63 | /// Creates a TermInfo based on current environment. |
| 64 | pub(crate) fn from_env() -> Result<TermInfo, Error> { |
| 65 | let term = match env::var("TERM" ) { |
| 66 | Ok(name) => TermInfo::from_name(&name), |
| 67 | Err(..) => return Err(Error::TermUnset), |
| 68 | }; |
| 69 | |
| 70 | if term.is_err() && env::var("MSYSCON" ).map_or(false, |s| "mintty.exe" == s) { |
| 71 | // msys terminal |
| 72 | Ok(msys_terminfo()) |
| 73 | } else { |
| 74 | term |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | /// Creates a TermInfo for the named terminal. |
| 79 | pub(crate) fn from_name(name: &str) -> Result<TermInfo, Error> { |
| 80 | if cfg!(miri) { |
| 81 | // Avoid all the work of parsing the terminfo (it's pretty slow under Miri), and just |
| 82 | // assume that the standard color codes work (like e.g. the 'colored' crate). |
| 83 | return Ok(TermInfo { |
| 84 | names: Default::default(), |
| 85 | bools: Default::default(), |
| 86 | numbers: Default::default(), |
| 87 | strings: Default::default(), |
| 88 | }); |
| 89 | } |
| 90 | |
| 91 | get_dbpath_for_term(name) |
| 92 | .ok_or_else(|| { |
| 93 | Error::IoError(io::const_error!(io::ErrorKind::NotFound, "terminfo file not found" )) |
| 94 | }) |
| 95 | .and_then(|p| TermInfo::from_path(&(*p))) |
| 96 | } |
| 97 | |
| 98 | /// Parse the given TermInfo. |
| 99 | pub(crate) fn from_path<P: AsRef<Path>>(path: P) -> Result<TermInfo, Error> { |
| 100 | Self::_from_path(path.as_ref()) |
| 101 | } |
| 102 | // Keep the metadata small |
| 103 | fn _from_path(path: &Path) -> Result<TermInfo, Error> { |
| 104 | let mut reader = File::open_buffered(path).map_err(Error::IoError)?; |
| 105 | parse(&mut reader, false).map_err(Error::MalformedTerminfo) |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | pub(crate) mod searcher; |
| 110 | |
| 111 | /// TermInfo format parsing. |
| 112 | pub(crate) mod parser { |
| 113 | //! ncurses-compatible compiled terminfo format parsing (term(5)) |
| 114 | pub(crate) mod compiled; |
| 115 | } |
| 116 | pub(crate) mod parm; |
| 117 | |
| 118 | /// A Terminal that knows how many colors it supports, with a reference to its |
| 119 | /// parsed Terminfo database record. |
| 120 | pub(crate) struct TerminfoTerminal<T> { |
| 121 | num_colors: u32, |
| 122 | out: T, |
| 123 | ti: TermInfo, |
| 124 | } |
| 125 | |
| 126 | impl<T: Write + Send> Terminal for TerminfoTerminal<T> { |
| 127 | fn fg(&mut self, color: color::Color) -> io::Result<bool> { |
| 128 | let color = self.dim_if_necessary(color); |
| 129 | if cfg!(miri) && color < 8 { |
| 130 | // The Miri logic for this only works for the most basic 8 colors, which we just assume |
| 131 | // the terminal will support. (`num_colors` is always 0 in Miri, so higher colors will |
| 132 | // just fail. But libtest doesn't use any higher colors anyway.) |
| 133 | return write!(self.out, " \x1B[3 {color}m" ).and(Ok(true)); |
| 134 | } |
| 135 | if self.num_colors > color { |
| 136 | return self.apply_cap("setaf" , &[Param::Number(color as i32)]); |
| 137 | } |
| 138 | Ok(false) |
| 139 | } |
| 140 | |
| 141 | fn reset(&mut self) -> io::Result<bool> { |
| 142 | if cfg!(miri) { |
| 143 | return write!(self.out, " \x1B[0m" ).and(Ok(true)); |
| 144 | } |
| 145 | // are there any terminals that have color/attrs and not sgr0? |
| 146 | // Try falling back to sgr, then op |
| 147 | let cmd = match ["sgr0" , "sgr" , "op" ].iter().find_map(|cap| self.ti.strings.get(*cap)) { |
| 148 | Some(op) => match expand(op, &[], &mut Variables::new()) { |
| 149 | Ok(cmd) => cmd, |
| 150 | Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)), |
| 151 | }, |
| 152 | None => return Ok(false), |
| 153 | }; |
| 154 | self.out.write_all(&cmd).and(Ok(true)) |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | impl<T: Write + Send> TerminfoTerminal<T> { |
| 159 | /// Creates a new TerminfoTerminal with the given TermInfo and Write. |
| 160 | pub(crate) fn new_with_terminfo(out: T, terminfo: TermInfo) -> TerminfoTerminal<T> { |
| 161 | let nc = if terminfo.strings.contains_key("setaf" ) && terminfo.strings.contains_key("setab" ) |
| 162 | { |
| 163 | terminfo.numbers.get("colors" ).map_or(0, |&n| n) |
| 164 | } else { |
| 165 | 0 |
| 166 | }; |
| 167 | |
| 168 | TerminfoTerminal { out, ti: terminfo, num_colors: nc } |
| 169 | } |
| 170 | |
| 171 | /// Creates a new TerminfoTerminal for the current environment with the given Write. |
| 172 | /// |
| 173 | /// Returns `None` when the terminfo cannot be found or parsed. |
| 174 | pub(crate) fn new(out: T) -> Option<TerminfoTerminal<T>> { |
| 175 | TermInfo::from_env().map(move |ti| TerminfoTerminal::new_with_terminfo(out, ti)).ok() |
| 176 | } |
| 177 | |
| 178 | fn dim_if_necessary(&self, color: color::Color) -> color::Color { |
| 179 | if color >= self.num_colors && (8..16).contains(&color) { color - 8 } else { color } |
| 180 | } |
| 181 | |
| 182 | fn apply_cap(&mut self, cmd: &str, params: &[Param]) -> io::Result<bool> { |
| 183 | match self.ti.strings.get(cmd) { |
| 184 | Some(cmd) => match expand(cmd, params, &mut Variables::new()) { |
| 185 | Ok(s) => self.out.write_all(&s).and(Ok(true)), |
| 186 | Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e)), |
| 187 | }, |
| 188 | None => Ok(false), |
| 189 | } |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | impl<T: Write> Write for TerminfoTerminal<T> { |
| 194 | fn write(&mut self, buf: &[u8]) -> io::Result<usize> { |
| 195 | self.out.write(buf) |
| 196 | } |
| 197 | |
| 198 | fn flush(&mut self) -> io::Result<()> { |
| 199 | self.out.flush() |
| 200 | } |
| 201 | } |
| 202 | |