1//! Terminfo database interface.
2
3use std::collections::HashMap;
4use std::env;
5use std::error;
6use std::fmt;
7use std::fs::File;
8use std::io::{self, prelude::*, BufReader};
9use std::path::Path;
10
11use super::color;
12use super::Terminal;
13
14use parm::{expand, Param, Variables};
15use parser::compiled::{msys_terminfo, parse};
16use searcher::get_dbpath_for_term;
17
18/// A parsed terminfo database entry.
19#[allow(unused)]
20#[derive(Debug)]
21pub(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)]
34pub(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
44impl 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
54impl 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
65impl 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
113pub(crate) mod searcher;
114
115/// TermInfo format parsing.
116pub(crate) mod parser {
117 //! ncurses-compatible compiled terminfo format parsing (term(5))
118 pub(crate) mod compiled;
119}
120pub(crate) mod parm;
121
122/// A Terminal that knows how many colors it supports, with a reference to its
123/// parsed Terminfo database record.
124pub(crate) struct TerminfoTerminal<T> {
125 num_colors: u32,
126 out: T,
127 ti: TermInfo,
128}
129
130impl<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
162impl<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
197impl<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