From f923b93694706730dd8395dec63e949ba441e0d9 Mon Sep 17 00:00:00 2001 From: Corey Richardson Date: Tue, 8 Apr 2014 11:18:10 -0400 Subject: [PATCH] term: add docs and windows support Closes #2807 --- mk/crates.mk | 2 +- src/libterm/lib.rs | 286 ++++++++++++++---------------------- src/libterm/terminfo/mod.rs | 186 +++++++++++++++++++++++ src/libterm/win.rs | 146 ++++++++++++++++++ 4 files changed, 440 insertions(+), 180 deletions(-) create mode 100644 src/libterm/win.rs diff --git a/mk/crates.mk b/mk/crates.mk index 943cd528fcd..895819a5a50 100644 --- a/mk/crates.mk +++ b/mk/crates.mk @@ -73,7 +73,7 @@ DEPS_arena := std collections DEPS_graphviz := std DEPS_glob := std DEPS_serialize := std collections log -DEPS_term := std collections +DEPS_term := std collections log DEPS_semver := std DEPS_uuid := std serialize rand DEPS_sync := std diff --git a/src/libterm/lib.rs b/src/libterm/lib.rs index 2d1b938ec37..8f5452c23f2 100644 --- a/src/libterm/lib.rs +++ b/src/libterm/lib.rs @@ -8,7 +8,32 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -//! Simple ANSI color library +//! Terminal formatting library. +//! +//! This crate provides the `Terminal` trait, which abstracts over an [ANSI +//! Termina][ansi] to provide color printing, among other things. There are two implementations, +//! the `TerminfoTerminal`, which uses control characters from a +//! [terminfo][ti] database, and `WinConsole`, which uses the [Win32 Console +//! API][win]. +//! +//! ## Example +//! +//! ```rust +//! extern crate term; +//! +//! fn main() { +//! let mut t = term::stdout(); +//! t.fg(term::color::GREEN); +//! println!("hello, "); +//! t.fg(term::color::RED); +//! println("world!"); +//! t.reset(); +//! } +//! ``` +//! +//! [ansi]: https://en.wikipedia.org/wiki/ANSI_escape_code +//! [win]: http://msdn.microsoft.com/en-us/library/windows/desktop/ms682010%28v=vs.85%29.aspx +//! [ti]: https://en.wikipedia.org/wiki/Terminfo #![crate_id = "term#0.11.0-pre"] #![comment = "Simple ANSI color library"] @@ -19,22 +44,76 @@ html_favicon_url = "http://www.rust-lang.org/favicon.ico", html_root_url = "http://static.rust-lang.org/doc/master")] -#![feature(macro_rules)] +#![feature(macro_rules, phase)] #![deny(missing_doc)] +#[phase(syntax, link)] extern crate log; extern crate collections; -use std::io; -use std::os; -use terminfo::TermInfo; -use terminfo::searcher::open; -use terminfo::parser::compiled::{parse, msys_terminfo}; -use terminfo::parm::{expand, Number, Variables}; +pub use terminfo::TerminfoTerminal; +#[cfg(windows)] +pub use win::WinConsole; + +use std::io::IoResult; pub mod terminfo; -// FIXME (#2807): Windows support. +#[cfg(windows)] +mod win; + +#[cfg(not(windows))] +/// Return a Terminal wrapping stdout, or None if a terminal couldn't be +/// opened. +pub fn stdout() -> Option<~Terminal<~Writer:Send>:Send> { + let ti: Option> + = Terminal::new(~std::io::stdout() as ~Writer:Send); + ti.map(|t| ~t as ~Terminal<~Writer:Send>:Send) +} + +#[cfg(windows)] +/// Return a Terminal wrapping stdout, or None if a terminal couldn't be +/// opened. +pub fn stdout() -> Option<~Terminal<~Writer:Send>:Send> { + let ti: Option> + = Terminal::new(~std::io::stdout() as ~Writer:Send); + + match ti { + Some(t) => Some(~t as ~Terminal<~Writer:Send>:Send), + None => { + let wc: Option> + = Terminal::new(~std::io::stdout() as ~Writer:Send); + wc.map(|w| ~w as ~Terminal<~Writer:Send>:Send) + } + } +} + +#[cfg(not(windows))] +/// Return a Terminal wrapping stderr, or None if a terminal couldn't be +/// opened. +pub fn stderr() -> Option<~Terminal<~Writer:Send>:Send> { + let ti: Option> + = Terminal::new(~std::io::stderr() as ~Writer:Send); + ti.map(|t| ~t as ~Terminal<~Writer:Send>:Send) +} + +#[cfg(windows)] +/// Return a Terminal wrapping stderr, or None if a terminal couldn't be +/// opened. +pub fn stderr() -> Option<~Terminal<~Writer:Send>:Send> { + let ti: Option> + = Terminal::new(~std::io::stderr() as ~Writer:Send); + + match ti { + Some(t) => Some(~t as ~Terminal<~Writer:Send>:Send), + None => { + let wc: Option> + = Terminal::new(~std::io::stderr() as ~Writer:Send); + wc.map(|w| ~w as ~Terminal<~Writer:Send>:Send) + } + } +} + /// Terminal color definitions pub mod color { @@ -91,72 +170,13 @@ pub mod attr { } } -fn cap_for_attr(attr: attr::Attr) -> &'static str { - match attr { - attr::Bold => "bold", - attr::Dim => "dim", - attr::Italic(true) => "sitm", - attr::Italic(false) => "ritm", - attr::Underline(true) => "smul", - attr::Underline(false) => "rmul", - attr::Blink => "blink", - attr::Standout(true) => "smso", - attr::Standout(false) => "rmso", - attr::Reverse => "rev", - attr::Secure => "invis", - attr::ForegroundColor(_) => "setaf", - attr::BackgroundColor(_) => "setab" - } -} +/// A terminal with similar capabilities to an ANSI Terminal +/// (foreground/background colors etc). +pub trait Terminal: Writer { + /// Returns `None` whenever the terminal cannot be created for some + /// reason. + fn new(out: T) -> Option; -/// A Terminal that knows how many colors it supports, with a reference to its -/// parsed TermInfo database record. -pub struct Terminal { - num_colors: u16, - out: T, - ti: Box, -} - -impl Terminal { - /// Returns a wrapped output stream (`Terminal`) as a `Result`. - /// - /// Returns `Err()` if the TERM environment variable is undefined. - /// TERM should be set to something like `xterm-color` or `screen-256color`. - /// - /// Returns `Err()` on failure to open the terminfo database correctly. - /// Also, in the event that the individual terminfo database entry can not - /// be parsed. - pub fn new(out: T) -> Result, StrBuf> { - let term = match os::getenv("TERM") { - Some(t) => t, - None => { - return Err("TERM environment variable undefined".to_strbuf()) - } - }; - - let mut file = match open(term) { - Ok(file) => file, - Err(err) => { - if "cygwin" == term { // msys terminal - return Ok(Terminal { - out: out, - ti: msys_terminfo(), - num_colors: 8 - }); - } - return Err(err); - } - }; - - let inf = try!(parse(&mut file, false)); - - let nc = if inf.strings.find_equiv(&("setaf")).is_some() - && inf.strings.find_equiv(&("setab")).is_some() { - inf.numbers.find_equiv(&("colors")).map_or(0, |&n| n) - } else { 0 }; - - return Ok(Terminal {out: out, ti: inf, num_colors: nc}); - } /// Sets the foreground color to the given color. /// /// If the color is a bright color, but the terminal only supports 8 colors, @@ -164,22 +184,8 @@ impl Terminal { /// /// Returns `Ok(true)` if the color was set, `Ok(false)` otherwise, and `Err(e)` /// if there was an I/O error. - pub fn fg(&mut self, color: color::Color) -> io::IoResult { - let color = self.dim_if_necessary(color); - if self.num_colors > color { - let s = expand(self.ti - .strings - .find_equiv(&("setaf")) - .unwrap() - .as_slice(), - [Number(color as int)], &mut Variables::new()); - if s.is_ok() { - try!(self.out.write(s.unwrap().as_slice())); - return Ok(true) - } - } - Ok(false) - } + fn fg(&mut self, color: color::Color) -> IoResult; + /// Sets the background color to the given color. /// /// If the color is a bright color, but the terminal only supports 8 colors, @@ -187,104 +193,26 @@ impl Terminal { /// /// Returns `Ok(true)` if the color was set, `Ok(false)` otherwise, and `Err(e)` /// if there was an I/O error. - pub fn bg(&mut self, color: color::Color) -> io::IoResult { - let color = self.dim_if_necessary(color); - if self.num_colors > color { - let s = expand(self.ti - .strings - .find_equiv(&("setab")) - .unwrap() - .as_slice(), - [Number(color as int)], &mut Variables::new()); - if s.is_ok() { - try!(self.out.write(s.unwrap().as_slice())); - return Ok(true) - } - } - Ok(false) - } + fn bg(&mut self, color: color::Color) -> IoResult; - /// Sets the given terminal attribute, if supported. - /// Returns `Ok(true)` if the attribute was supported, `Ok(false)` otherwise, - /// and `Err(e)` if there was an I/O error. - pub fn attr(&mut self, attr: attr::Attr) -> io::IoResult { - match attr { - attr::ForegroundColor(c) => self.fg(c), - attr::BackgroundColor(c) => self.bg(c), - _ => { - let cap = cap_for_attr(attr); - let parm = self.ti.strings.find_equiv(&cap); - if parm.is_some() { - let s = expand(parm.unwrap().as_slice(), - [], - &mut Variables::new()); - if s.is_ok() { - try!(self.out.write(s.unwrap().as_slice())); - return Ok(true) - } - } - Ok(false) - } - } - } + /// Sets the given terminal attribute, if supported. Returns `Ok(true)` + /// if the attribute was supported, `Ok(false)` otherwise, and `Err(e)` if + /// there was an I/O error. + fn attr(&mut self, attr: attr::Attr) -> IoResult; /// Returns whether the given terminal attribute is supported. - pub fn supports_attr(&self, attr: attr::Attr) -> bool { - match attr { - attr::ForegroundColor(_) | attr::BackgroundColor(_) => { - self.num_colors > 0 - } - _ => { - let cap = cap_for_attr(attr); - self.ti.strings.find_equiv(&cap).is_some() - } - } - } + fn supports_attr(&self, attr: attr::Attr) -> bool; /// Resets all terminal attributes and color to the default. /// Returns `Ok()`. - pub fn reset(&mut self) -> io::IoResult<()> { - let mut cap = self.ti.strings.find_equiv(&("sgr0")); - if cap.is_none() { - // are there any terminals that have color/attrs and not sgr0? - // Try falling back to sgr, then op - cap = self.ti.strings.find_equiv(&("sgr")); - if cap.is_none() { - cap = self.ti.strings.find_equiv(&("op")); - } - } - let s = cap.map_or(Err("can't find terminfo capability \ - `sgr0`".to_strbuf()), |op| { - expand(op.as_slice(), [], &mut Variables::new()) - }); - if s.is_ok() { - return self.out.write(s.unwrap().as_slice()) - } - Ok(()) - } + fn reset(&mut self) -> IoResult<()>; - fn dim_if_necessary(&self, color: color::Color) -> color::Color { - if color >= self.num_colors && color >= 8 && color < 16 { - color-8 - } else { color } - } - - /// Returns the contained stream - pub fn unwrap(self) -> T { self.out } + /// Returns the contained stream, destroying the `Terminal` + fn unwrap(self) -> T; /// Gets an immutable reference to the stream inside - pub fn get_ref<'a>(&'a self) -> &'a T { &self.out } + fn get_ref<'a>(&'a self) -> &'a T; /// Gets a mutable reference to the stream inside - pub fn get_mut<'a>(&'a mut self) -> &'a mut T { &mut self.out } -} - -impl Writer for Terminal { - fn write(&mut self, buf: &[u8]) -> io::IoResult<()> { - self.out.write(buf) - } - - fn flush(&mut self) -> io::IoResult<()> { - self.out.flush() - } + fn get_mut<'a>(&'a mut self) -> &'a mut T; } diff --git a/src/libterm/terminfo/mod.rs b/src/libterm/terminfo/mod.rs index 46dd3978531..93a7657fae9 100644 --- a/src/libterm/terminfo/mod.rs +++ b/src/libterm/terminfo/mod.rs @@ -11,6 +11,16 @@ //! Terminfo database interface. use collections::HashMap; +use std::io::IoResult; +use std::os; + +use attr; +use color; +use Terminal; +use self::searcher::open; +use self::parser::compiled::{parse, msys_terminfo}; +use self::parm::{expand, Number, Variables}; + /// A parsed terminfo database entry. pub struct TermInfo { @@ -32,3 +42,179 @@ pub mod parser { pub mod compiled; } pub mod parm; + + +fn cap_for_attr(attr: attr::Attr) -> &'static str { + match attr { + attr::Bold => "bold", + attr::Dim => "dim", + attr::Italic(true) => "sitm", + attr::Italic(false) => "ritm", + attr::Underline(true) => "smul", + attr::Underline(false) => "rmul", + attr::Blink => "blink", + attr::Standout(true) => "smso", + attr::Standout(false) => "rmso", + attr::Reverse => "rev", + attr::Secure => "invis", + attr::ForegroundColor(_) => "setaf", + attr::BackgroundColor(_) => "setab" + } +} + +/// A Terminal that knows how many colors it supports, with a reference to its +/// parsed Terminfo database record. +pub struct TerminfoTerminal { + num_colors: u16, + out: T, + ti: ~TermInfo +} + +impl Terminal for TerminfoTerminal { + fn new(out: T) -> Option> { + let term = match os::getenv("TERM") { + Some(t) => t, + None => { + debug!("TERM environment variable not defined"); + return None; + } + }; + + let entry = open(term); + if entry.is_err() { + if os::getenv("MSYSCON").map_or(false, |s| "mintty.exe" == s) { + // msys terminal + return Some(TerminfoTerminal {out: out, ti: msys_terminfo(), num_colors: 8}); + } + debug!("error finding terminfo entry: {}", entry.unwrap_err()); + return None; + } + + let mut file = entry.unwrap(); + let ti = parse(&mut file, false); + if ti.is_err() { + debug!("error parsing terminfo entry: {}", ti.unwrap_err()); + return None; + } + + let inf = ti.unwrap(); + let nc = if inf.strings.find_equiv(&("setaf")).is_some() + && inf.strings.find_equiv(&("setab")).is_some() { + inf.numbers.find_equiv(&("colors")).map_or(0, |&n| n) + } else { 0 }; + + return Some(TerminfoTerminal {out: out, ti: inf, num_colors: nc}); + } + + fn fg(&mut self, color: color::Color) -> IoResult { + let color = self.dim_if_necessary(color); + if self.num_colors > color { + let s = expand(self.ti + .strings + .find_equiv(&("setaf")) + .unwrap() + .as_slice(), + [Number(color as int)], &mut Variables::new()); + if s.is_ok() { + try!(self.out.write(s.unwrap().as_slice())); + return Ok(true) + } + } + Ok(false) + } + + fn bg(&mut self, color: color::Color) -> IoResult { + let color = self.dim_if_necessary(color); + if self.num_colors > color { + let s = expand(self.ti + .strings + .find_equiv(&("setab")) + .unwrap() + .as_slice(), + [Number(color as int)], &mut Variables::new()); + if s.is_ok() { + try!(self.out.write(s.unwrap().as_slice())); + return Ok(true) + } + } + Ok(false) + } + + fn attr(&mut self, attr: attr::Attr) -> IoResult { + match attr { + attr::ForegroundColor(c) => self.fg(c), + attr::BackgroundColor(c) => self.bg(c), + _ => { + let cap = cap_for_attr(attr); + let parm = self.ti.strings.find_equiv(&cap); + if parm.is_some() { + let s = expand(parm.unwrap().as_slice(), + [], + &mut Variables::new()); + if s.is_ok() { + try!(self.out.write(s.unwrap().as_slice())); + return Ok(true) + } + } + Ok(false) + } + } + } + + fn supports_attr(&self, attr: attr::Attr) -> bool { + match attr { + attr::ForegroundColor(_) | attr::BackgroundColor(_) => { + self.num_colors > 0 + } + _ => { + let cap = cap_for_attr(attr); + self.ti.strings.find_equiv(&cap).is_some() + } + } + } + + fn reset(&mut self) -> IoResult<()> { + let mut cap = self.ti.strings.find_equiv(&("sgr0")); + if cap.is_none() { + // are there any terminals that have color/attrs and not sgr0? + // Try falling back to sgr, then op + cap = self.ti.strings.find_equiv(&("sgr")); + if cap.is_none() { + cap = self.ti.strings.find_equiv(&("op")); + } + } + let s = cap.map_or(Err(~"can't find terminfo capability `sgr0`"), |op| { + expand(op.as_slice(), [], &mut Variables::new()) + }); + if s.is_ok() { + return self.out.write(s.unwrap().as_slice()) + } + Ok(()) + } + + fn unwrap(self) -> T { self.out } + + fn get_ref<'a>(&'a self) -> &'a T { &self.out } + + fn get_mut<'a>(&'a mut self) -> &'a mut T { &mut self.out } +} + +impl TerminfoTerminal { + fn dim_if_necessary(&self, color: color::Color) -> color::Color { + if color >= self.num_colors && color >= 8 && color < 16 { + color-8 + } else { color } + } +} + + +impl Writer for TerminfoTerminal { + fn write(&mut self, buf: &[u8]) -> IoResult<()> { + self.out.write(buf) + } + + fn flush(&mut self) -> IoResult<()> { + self.out.flush() + } +} + diff --git a/src/libterm/win.rs b/src/libterm/win.rs new file mode 100644 index 00000000000..063f93cebac --- /dev/null +++ b/src/libterm/win.rs @@ -0,0 +1,146 @@ +// Copyright 2013-2014 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Windows console handling + +// FIXME (#13400): this is only a tiny fraction of the win32 console api + +extern crate libc; + +use std::io::IoResult; + +use attr; +use color; +use Terminal; + +pub struct WinConsole { + buf: T, + foreground: color::Color, + background: color::Color, +} + +#[link(name = "kernel32")] +extern "system" { + fn SetConsoleTextAttribute(handle: libc::HANDLE, attr: libc::WORD) -> libc::BOOL; + fn GetStdHandle(which: libc::DWORD) -> libc::HANDLE; +} + +fn color_to_bits(color: color::Color) -> u16 { + // magic numbers from mingw-w64's wincon.h + + let bits = match color % 8 { + color::BLACK => 0, + color::BLUE => 0x1, + color::GREEN => 0x2, + color::RED => 0x4, + color::YELLOW => 0x2 | 0x4, + color::MAGENTA => 0x1 | 0x4, + color::CYAN => 0x1 | 0x2, + color::WHITE => 0x1 | 0x2 | 0x4, + _ => unreachable!() + }; + + if color >= 8 { + bits | 0x8 + } else { + bits + } +} + +impl WinConsole { + fn apply(&mut self) { + let mut accum: libc::WORD = 0; + accum |= color_to_bits(self.foreground); + accum |= color_to_bits(self.background) << 4; + + unsafe { + // Magic -11 means stdout, from + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms683231%28v=vs.85%29.aspx + // + // You may be wondering, "but what about stderr?", and the answer + // to that is that setting terminal attributes on the stdout + // handle also sets them for stderr, since they go to the same + // terminal! Admittedly, this is fragile, since stderr could be + // redirected to a different console. This is good enough for + // rustc though. See #13400. + let out = GetStdHandle(-11); + SetConsoleTextAttribute(out, accum); + } + } +} + +impl Writer for WinConsole { + fn write(&mut self, buf: &[u8]) -> IoResult<()> { + self.buf.write(buf) + } + + fn flush(&mut self) -> IoResult<()> { + self.buf.flush() + } +} + +impl Terminal for WinConsole { + fn new(out: T) -> Option> { + Some(WinConsole { buf: out, foreground: color::WHITE, background: color::BLACK }) + } + + fn fg(&mut self, color: color::Color) -> IoResult { + self.foreground = color; + self.apply(); + + Ok(true) + } + + fn bg(&mut self, color: color::Color) -> IoResult { + self.background = color; + self.apply(); + + Ok(true) + } + + fn attr(&mut self, attr: attr::Attr) -> IoResult { + match attr { + attr::ForegroundColor(f) => { + self.foreground = f; + self.apply(); + Ok(true) + }, + attr::BackgroundColor(b) => { + self.background = b; + self.apply(); + Ok(true) + }, + _ => Ok(false) + } + } + + fn supports_attr(&self, attr: attr::Attr) -> bool { + // it claims support for underscore and reverse video, but I can't get + // it to do anything -cmr + match attr { + attr::ForegroundColor(_) | attr::BackgroundColor(_) => true, + _ => false + } + } + + fn reset(&mut self) -> IoResult<()> { + self.foreground = color::WHITE; + self.background = color::BLACK; + self.apply(); + + Ok(()) + } + + fn unwrap(self) -> T { self.buf } + + fn get_ref<'a>(&'a self) -> &'a T { &self.buf } + + fn get_mut<'a>(&'a mut self) -> &'a mut T { &mut self.buf } +}