1 | // Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT |
2 | // file at the top-level directory of this distribution and at |
3 | // http://rust-lang.org/COPYRIGHT. |
4 | // |
5 | // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or |
6 | // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
7 | // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your |
8 | // option. This file may not be copied, modified, or distributed |
9 | // except according to those terms. |
10 | |
11 | //! Terminfo database interface. |
12 | |
13 | use std::collections::HashMap; |
14 | use std::env; |
15 | use std::fs::File; |
16 | use std::io; |
17 | use std::io::prelude::*; |
18 | use std::io::BufReader; |
19 | use std::path::Path; |
20 | |
21 | #[cfg (windows)] |
22 | use crate::win; |
23 | |
24 | use self::parm::{expand, Param, Variables}; |
25 | use self::parser::compiled::parse; |
26 | use self::searcher::get_dbpath_for_term; |
27 | use self::Error::*; |
28 | use crate::color; |
29 | use crate::Attr; |
30 | use crate::Result; |
31 | use crate::Terminal; |
32 | |
33 | /// Returns true if the named terminal supports basic ANSI escape codes. |
34 | fn is_ansi(name: &str) -> bool { |
35 | // SORTED! We binary search this. |
36 | static ANSI_TERM_PREFIX: &[&str] = &[ |
37 | "Eterm" , "ansi" , "eterm" , "iterm" , "konsole" , "linux" , "mrxvt" , "msyscon" , "rxvt" , |
38 | "screen" , "tmux" , "xterm" , |
39 | ]; |
40 | match ANSI_TERM_PREFIX.binary_search(&name) { |
41 | Ok(_) => true, |
42 | Err(0) => false, |
43 | Err(idx: usize) => name.starts_with(ANSI_TERM_PREFIX[idx - 1]), |
44 | } |
45 | } |
46 | |
47 | /// A parsed terminfo database entry. |
48 | #[derive (Debug, Clone)] |
49 | pub struct TermInfo { |
50 | /// Names for the terminal |
51 | pub names: Vec<String>, |
52 | /// Map of capability name to boolean value |
53 | pub bools: HashMap<&'static str, bool>, |
54 | /// Map of capability name to numeric value |
55 | pub numbers: HashMap<&'static str, u32>, |
56 | /// Map of capability name to raw (unexpanded) string |
57 | pub strings: HashMap<&'static str, Vec<u8>>, |
58 | } |
59 | |
60 | impl TermInfo { |
61 | /// Create a `TermInfo` based on current environment. |
62 | pub fn from_env() -> Result<TermInfo> { |
63 | let term_var = env::var("TERM" ).ok(); |
64 | let term_name = term_var.as_ref().map(|s| &**s).or_else(|| { |
65 | env::var("MSYSCON" ).ok().and_then(|s| { |
66 | if s == "mintty.exe" { |
67 | Some("msyscon" ) |
68 | } else { |
69 | None |
70 | } |
71 | }) |
72 | }); |
73 | |
74 | #[cfg (windows)] |
75 | { |
76 | if term_name.is_none() && win::supports_ansi() { |
77 | // Microsoft people seem to be fine with pretending to be xterm: |
78 | // https://github.com/Microsoft/WSL/issues/1446 |
79 | // The basic ANSI fallback terminal will be uses. |
80 | return TermInfo::from_name("xterm" ); |
81 | } |
82 | } |
83 | |
84 | if let Some(term_name) = term_name { |
85 | TermInfo::from_name(term_name) |
86 | } else { |
87 | Err(crate::Error::TermUnset) |
88 | } |
89 | } |
90 | |
91 | /// Create a `TermInfo` for the named terminal. |
92 | pub fn from_name(name: &str) -> Result<TermInfo> { |
93 | if let Some(path) = get_dbpath_for_term(name) { |
94 | match TermInfo::from_path(&path) { |
95 | Ok(term) => return Ok(term), |
96 | // Skip IO Errors (e.g., permission denied). |
97 | Err(crate::Error::Io(_)) => {} |
98 | // Don't ignore malformed terminfo databases. |
99 | Err(e) => return Err(e), |
100 | } |
101 | } |
102 | // Basic ANSI fallback terminal. |
103 | if is_ansi(name) { |
104 | let mut strings = HashMap::new(); |
105 | strings.insert("sgr0" , b" \x1B[0m" .to_vec()); |
106 | strings.insert("bold" , b" \x1B[1m" .to_vec()); |
107 | strings.insert("setaf" , b" \x1B[3%p1%dm" .to_vec()); |
108 | strings.insert("setab" , b" \x1B[4%p1%dm" .to_vec()); |
109 | |
110 | let mut numbers = HashMap::new(); |
111 | numbers.insert("colors" , 8); |
112 | |
113 | Ok(TermInfo { |
114 | names: vec![name.to_owned()], |
115 | bools: HashMap::new(), |
116 | numbers: numbers, |
117 | strings: strings, |
118 | }) |
119 | } else { |
120 | Err(crate::Error::TerminfoEntryNotFound) |
121 | } |
122 | } |
123 | |
124 | /// Parse the given `TermInfo`. |
125 | pub fn from_path<P: AsRef<Path>>(path: P) -> Result<TermInfo> { |
126 | Self::_from_path(path.as_ref()) |
127 | } |
128 | // Keep the metadata small |
129 | // (That is, this uses a &Path so that this function need not be instantiated |
130 | // for every type |
131 | // which implements AsRef<Path>. One day, if/when rustc is a bit smarter, it |
132 | // might do this for |
133 | // us. Alas. ) |
134 | fn _from_path(path: &Path) -> Result<TermInfo> { |
135 | let file = File::open(path).map_err(crate::Error::Io)?; |
136 | let mut reader = BufReader::new(file); |
137 | parse(&mut reader, false) |
138 | } |
139 | |
140 | /// Retrieve a capability `cmd` and expand it with `params`, writing result to `out`. |
141 | pub fn apply_cap(&self, cmd: &str, params: &[Param], out: &mut dyn io::Write) -> Result<()> { |
142 | match self.strings.get(cmd) { |
143 | Some(cmd) => match expand(cmd, params, &mut Variables::new()) { |
144 | Ok(s) => { |
145 | out.write_all(&s)?; |
146 | Ok(()) |
147 | } |
148 | Err(e) => Err(e.into()), |
149 | }, |
150 | None => Err(crate::Error::NotSupported), |
151 | } |
152 | } |
153 | |
154 | /// Write the reset string to `out`. |
155 | pub fn reset(&self, out: &mut dyn io::Write) -> Result<()> { |
156 | // are there any terminals that have color/attrs and not sgr0? |
157 | // Try falling back to sgr, then op |
158 | let cmd = match [ |
159 | ("sgr0" , &[] as &[Param]), |
160 | ("sgr" , &[Param::Number(0)]), |
161 | ("op" , &[]), |
162 | ] |
163 | .iter() |
164 | .filter_map(|&(cap, params)| self.strings.get(cap).map(|c| (c, params))) |
165 | .next() |
166 | { |
167 | Some((op, params)) => match expand(op, params, &mut Variables::new()) { |
168 | Ok(cmd) => cmd, |
169 | Err(e) => return Err(e.into()), |
170 | }, |
171 | None => return Err(crate::Error::NotSupported), |
172 | }; |
173 | out.write_all(&cmd)?; |
174 | Ok(()) |
175 | } |
176 | } |
177 | |
178 | #[derive (Debug, Eq, PartialEq)] |
179 | /// An error from parsing a terminfo entry |
180 | pub enum Error { |
181 | /// The "magic" number at the start of the file was wrong. |
182 | /// |
183 | /// It should be `0x11A` (16bit numbers) or `0x21e` (32bit numbers) |
184 | BadMagic(u16), |
185 | /// The names in the file were not valid UTF-8. |
186 | /// |
187 | /// In theory these should only be ASCII, but to work with the Rust `str` type, we treat them |
188 | /// as UTF-8. This is valid, except when a terminfo file decides to be invalid. This hasn't |
189 | /// been encountered in the wild. |
190 | NotUtf8(::std::str::Utf8Error), |
191 | /// The names section of the file was empty |
192 | ShortNames, |
193 | /// More boolean parameters are present in the file than this crate knows how to interpret. |
194 | TooManyBools, |
195 | /// More number parameters are present in the file than this crate knows how to interpret. |
196 | TooManyNumbers, |
197 | /// More string parameters are present in the file than this crate knows how to interpret. |
198 | TooManyStrings, |
199 | /// The length of some field was not >= -1. |
200 | InvalidLength, |
201 | /// The names table was missing a trailing null terminator. |
202 | NamesMissingNull, |
203 | /// The strings table was missing a trailing null terminator. |
204 | StringsMissingNull, |
205 | } |
206 | |
207 | impl ::std::fmt::Display for Error { |
208 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { |
209 | match *self { |
210 | BadMagic(v: u16) => write!(f, "bad magic number {:x} in terminfo header" , v), |
211 | ShortNames => f.write_str(data:"no names exposed, need at least one" ), |
212 | TooManyBools => f.write_str(data:"more boolean properties than libterm knows about" ), |
213 | TooManyNumbers => f.write_str(data:"more number properties than libterm knows about" ), |
214 | TooManyStrings => f.write_str(data:"more string properties than libterm knows about" ), |
215 | InvalidLength => f.write_str(data:"invalid length field value, must be >= -1" ), |
216 | NotUtf8(ref e: &Utf8Error) => e.fmt(f), |
217 | NamesMissingNull => f.write_str(data:"names table missing NUL terminator" ), |
218 | StringsMissingNull => f.write_str(data:"string table missing NUL terminator" ), |
219 | } |
220 | } |
221 | } |
222 | |
223 | impl ::std::convert::From<::std::string::FromUtf8Error> for Error { |
224 | fn from(v: ::std::string::FromUtf8Error) -> Self { |
225 | NotUtf8(v.utf8_error()) |
226 | } |
227 | } |
228 | |
229 | impl ::std::error::Error for Error { |
230 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { |
231 | match *self { |
232 | NotUtf8(ref e: &Utf8Error) => Some(e), |
233 | _ => None, |
234 | } |
235 | } |
236 | } |
237 | |
238 | pub mod searcher; |
239 | |
240 | /// `TermInfo` format parsing. |
241 | pub mod parser { |
242 | //! ncurses-compatible compiled terminfo format parsing (term(5)) |
243 | pub mod compiled; |
244 | mod names; |
245 | } |
246 | pub mod parm; |
247 | |
248 | fn cap_for_attr(attr: Attr) -> &'static str { |
249 | match attr { |
250 | Attr::Bold => "bold" , |
251 | Attr::Dim => "dim" , |
252 | Attr::Italic(true) => "sitm" , |
253 | Attr::Italic(false) => "ritm" , |
254 | Attr::Underline(true) => "smul" , |
255 | Attr::Underline(false) => "rmul" , |
256 | Attr::Blink => "blink" , |
257 | Attr::Standout(true) => "smso" , |
258 | Attr::Standout(false) => "rmso" , |
259 | Attr::Reverse => "rev" , |
260 | Attr::Secure => "invis" , |
261 | Attr::ForegroundColor(_) => "setaf" , |
262 | Attr::BackgroundColor(_) => "setab" , |
263 | } |
264 | } |
265 | |
266 | /// A Terminal that knows how many colors it supports, with a reference to its |
267 | /// parsed Terminfo database record. |
268 | #[derive (Clone, Debug)] |
269 | pub struct TerminfoTerminal<T> { |
270 | num_colors: u32, |
271 | out: T, |
272 | ti: TermInfo, |
273 | } |
274 | |
275 | impl<T: Write> Terminal for TerminfoTerminal<T> { |
276 | type Output = T; |
277 | fn fg(&mut self, color: color::Color) -> Result<()> { |
278 | let color = self.dim_if_necessary(color); |
279 | if self.num_colors > color { |
280 | return self |
281 | .ti |
282 | .apply_cap("setaf" , &[Param::Number(color as i32)], &mut self.out); |
283 | } |
284 | Err(crate::Error::ColorOutOfRange) |
285 | } |
286 | |
287 | fn bg(&mut self, color: color::Color) -> Result<()> { |
288 | let color = self.dim_if_necessary(color); |
289 | if self.num_colors > color { |
290 | return self |
291 | .ti |
292 | .apply_cap("setab" , &[Param::Number(color as i32)], &mut self.out); |
293 | } |
294 | Err(crate::Error::ColorOutOfRange) |
295 | } |
296 | |
297 | fn attr(&mut self, attr: Attr) -> Result<()> { |
298 | match attr { |
299 | Attr::ForegroundColor(c) => self.fg(c), |
300 | Attr::BackgroundColor(c) => self.bg(c), |
301 | _ => self.ti.apply_cap(cap_for_attr(attr), &[], &mut self.out), |
302 | } |
303 | } |
304 | |
305 | fn supports_attr(&self, attr: Attr) -> bool { |
306 | match attr { |
307 | Attr::ForegroundColor(_) | Attr::BackgroundColor(_) => self.num_colors > 0, |
308 | _ => { |
309 | let cap = cap_for_attr(attr); |
310 | self.ti.strings.get(cap).is_some() |
311 | } |
312 | } |
313 | } |
314 | |
315 | fn reset(&mut self) -> Result<()> { |
316 | self.ti.reset(&mut self.out) |
317 | } |
318 | |
319 | fn supports_reset(&self) -> bool { |
320 | ["sgr0" , "sgr" , "op" ] |
321 | .iter() |
322 | .any(|&cap| self.ti.strings.get(cap).is_some()) |
323 | } |
324 | |
325 | fn supports_color(&self) -> bool { |
326 | self.num_colors > 0 && self.supports_reset() |
327 | } |
328 | |
329 | fn cursor_up(&mut self) -> Result<()> { |
330 | self.ti.apply_cap("cuu1" , &[], &mut self.out) |
331 | } |
332 | |
333 | fn delete_line(&mut self) -> Result<()> { |
334 | self.ti.apply_cap("el" , &[], &mut self.out) |
335 | } |
336 | |
337 | fn carriage_return(&mut self) -> Result<()> { |
338 | self.ti.apply_cap("cr" , &[], &mut self.out) |
339 | } |
340 | |
341 | fn get_ref(&self) -> &T { |
342 | &self.out |
343 | } |
344 | |
345 | fn get_mut(&mut self) -> &mut T { |
346 | &mut self.out |
347 | } |
348 | |
349 | fn into_inner(self) -> T |
350 | where |
351 | Self: Sized, |
352 | { |
353 | self.out |
354 | } |
355 | } |
356 | |
357 | impl<T: Write> TerminfoTerminal<T> { |
358 | /// Create a new TerminfoTerminal with the given TermInfo and Write. |
359 | pub fn new_with_terminfo(out: T, terminfo: TermInfo) -> TerminfoTerminal<T> { |
360 | let nc = if terminfo.strings.contains_key("setaf" ) && terminfo.strings.contains_key("setab" ) |
361 | { |
362 | terminfo.numbers.get("colors" ).map_or(0, |&n| n) |
363 | } else { |
364 | 0 |
365 | }; |
366 | |
367 | TerminfoTerminal { |
368 | out: out, |
369 | ti: terminfo, |
370 | num_colors: nc as u32, |
371 | } |
372 | } |
373 | |
374 | /// Create a new TerminfoTerminal for the current environment with the given Write. |
375 | /// |
376 | /// Returns `None` when the terminfo cannot be found or parsed. |
377 | pub fn new(out: T) -> Option<TerminfoTerminal<T>> { |
378 | TermInfo::from_env() |
379 | .map(move |ti| TerminfoTerminal::new_with_terminfo(out, ti)) |
380 | .ok() |
381 | } |
382 | |
383 | fn dim_if_necessary(&self, color: color::Color) -> color::Color { |
384 | if color >= self.num_colors && color >= 8 && color < 16 { |
385 | color - 8 |
386 | } else { |
387 | color |
388 | } |
389 | } |
390 | } |
391 | |
392 | impl<T: Write> Write for TerminfoTerminal<T> { |
393 | fn write(&mut self, buf: &[u8]) -> io::Result<usize> { |
394 | self.out.write(buf) |
395 | } |
396 | |
397 | fn flush(&mut self) -> io::Result<()> { |
398 | self.out.flush() |
399 | } |
400 | } |
401 | |