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 | |