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