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