diff --git a/src/libsyntax/diagnostic.rs b/src/libsyntax/diagnostic.rs index f09072e0bc605..f4f256082982c 100644 --- a/src/libsyntax/diagnostic.rs +++ b/src/libsyntax/diagnostic.rs @@ -252,7 +252,7 @@ pub struct EmitterWriter { } enum Destination { - Terminal(term::Terminal), + Terminal(~term::TerminalOps:Send), Raw(~Writer:Send), } @@ -260,7 +260,7 @@ impl EmitterWriter { pub fn stderr() -> EmitterWriter { let stderr = io::stderr(); if stderr.get_ref().isatty() { - let dst = match term::Terminal::new(stderr.unwrap()) { + let dst = match term::new_terminal(stderr.unwrap()) { Ok(t) => Terminal(t), Err(..) => Raw(box io::stderr()), }; diff --git a/src/libterm/lib.rs b/src/libterm/lib.rs index 9e3be403065ae..2a943ae0e0fae 100644 --- a/src/libterm/lib.rs +++ b/src/libterm/lib.rs @@ -8,7 +8,7 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -//! Simple ANSI color library +//! Terminal text decoration library #![crate_id = "term#0.11-pre"] #![comment = "Simple ANSI color library"] @@ -24,6 +24,7 @@ #![deny(missing_doc)] extern crate collections; +#[cfg(windows)] extern crate libc; use std::io; use std::os; @@ -34,8 +35,6 @@ use terminfo::parm::{expand, Number, Variables}; pub mod terminfo; -// FIXME (#2807): Windows support. - /// Terminal color definitions pub mod color { /// Number for a terminal color @@ -109,60 +108,118 @@ fn cap_for_attr(attr: attr::Attr) -> &'static str { } } -/// A Terminal that knows how many colors it supports, with a reference to its -/// parsed TermInfo database record. -pub struct Terminal { +struct Terminal { num_colors: u16, out: T, ti: ~TermInfo } -impl Terminal { - /// Returns a wrapped output stream (`Terminal`) as a `Result`. +/// Terminal operations +pub trait TerminalOps : Writer { + /// Sets the foreground color to the given color. /// - /// Returns `Err()` if the TERM environment variable is undefined. - /// TERM should be set to something like `xterm-color` or `screen-256color`. + /// If the color is a bright color, but the terminal only supports 8 colors, + /// the corresponding normal color will be used instead. /// - /// 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, ~str> { - let term = match os::getenv("TERM") { - Some(t) => t, - None => return Err("TERM environment variable undefined".to_owned()) - }; - - 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 }; + /// Returns `Ok(true)` if the color was set, `Ok(false)` otherwise, and `Err(e)` + /// if there was an I/O error. + fn fg(&mut self, color: color::Color) -> io::IoResult; - return Ok(Terminal {out: out, ti: inf, num_colors: nc}); - } - /// Sets the foreground color to the given color. + /// Sets the background color to the given color. /// /// If the color is a bright color, but the terminal only supports 8 colors, /// the corresponding normal color will be used instead. /// /// 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 { + fn bg(&mut self, color: color::Color) -> io::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. + fn attr(&mut self, attr: attr::Attr) -> io::IoResult; + + /// Returns whether the given terminal attribute is supported. + fn supports_attr(&self, attr: attr::Attr) -> bool; + + /// Resets all terminal attributes and color to the default. + /// Returns `Ok()`. + fn reset(&mut self) -> io::IoResult<()>; + + /// Gets an immutable reference to the stream inside + fn get_ref<'a>(&'a self) -> &'a T; + + /// Gets a mutable reference to the stream inside + fn get_mut<'a>(&'a mut self) -> &'a mut T; +} + +/// Returns a wrapped output stream (`~TerminalOps`) 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. +/// +/// On Windows, if the TERM environment variable is undefined, or the +/// variable is defined and is set to "cygwin", it assumes the program is +/// running on Windows console and returns platform-specific implementation. +/// Otherwise, it will behave like other platforms. +pub fn new_terminal(out: T) -> Result<~TerminalOps:Send, ~str> { + new_terminal_platform(out) +} + +#[cfg(not(windows))] +fn new_terminal_platform(out: T) -> Result<~TerminalOps:Send, ~str> { + let term = match os::getenv("TERM") { + Some(t) => t, + None => return Err("TERM environment variable undefined".to_owned()) + }; + open_terminal(out, term) +} + +#[cfg(windows)] +fn new_terminal_platform(out: T) -> Result<~TerminalOps:Send, ~str> { + match os::getenv("TERM") { + Some(term) => { + if term == "cygwin".to_owned() { // msys terminal + windows::new_console(out) + } else { + open_terminal(out, term) + } + }, + None => windows::new_console(out) + } +} + +fn open_terminal(out: T, term: ~str) -> Result<~TerminalOps:Send, ~str> { + let mut file = match open(term) { + Ok(file) => file, + Err(err) => { + if "xterm" == term { // MSYS rxvt + return Ok(box Terminal { + out: out, + ti: msys_terminfo(), + num_colors: 8 + } as ~TerminalOps:Send); + } + 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(box Terminal {out: out, ti: inf, num_colors: nc} as ~TerminalOps:Send); +} + +impl TerminalOps for Terminal { + 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 @@ -178,14 +235,8 @@ impl Terminal { } Ok(false) } - /// Sets the background color to the given color. - /// - /// If the color is a bright color, but the terminal only supports 8 colors, - /// the corresponding normal color will be used instead. - /// - /// 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 { + + 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 @@ -202,10 +253,7 @@ impl Terminal { 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. - pub fn attr(&mut self, attr: attr::Attr) -> io::IoResult { + fn attr(&mut self, attr: attr::Attr) -> io::IoResult { match attr { attr::ForegroundColor(c) => self.fg(c), attr::BackgroundColor(c) => self.bg(c), @@ -226,8 +274,7 @@ impl Terminal { } } - /// Returns whether the given terminal attribute is supported. - pub fn supports_attr(&self, attr: attr::Attr) -> bool { + fn supports_attr(&self, attr: attr::Attr) -> bool { match attr { attr::ForegroundColor(_) | attr::BackgroundColor(_) => { self.num_colors > 0 @@ -239,9 +286,7 @@ impl Terminal { } } - /// Resets all terminal attributes and color to the default. - /// Returns `Ok()`. - pub fn reset(&mut self) -> io::IoResult<()> { + 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? @@ -260,20 +305,17 @@ impl Terminal { Ok(()) } + fn get_ref<'a>(&'a self) -> &'a T { &self.out } + + fn get_mut<'a>(&'a mut self) -> &'a mut T { &mut self.out } +} + +impl Terminal { 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 } - - /// Gets an immutable reference to the stream inside - pub fn get_ref<'a>(&'a self) -> &'a T { &self.out } - - /// 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 { @@ -285,3 +327,240 @@ impl Writer for Terminal { self.out.flush() } } + +#[cfg(windows)] +#[allow(non_camel_case_types)] +#[allow(uppercase_variables)] +mod windows { + use std::io; + use std::os::win32::as_utf16_p; + use std::ptr; + use libc::{BOOL, HANDLE, WORD}; + use libc::{GENERIC_READ, GENERIC_WRITE, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, INVALID_HANDLE_VALUE}; + use libc::{CreateFileW, CloseHandle}; + + use color; + use attr; + + use TerminalOps; + #[cfg(test)] use new_terminal; + + pub struct WindowsConsole { + out: T, + conout: HANDLE, + orig_attr: WORD + } + + // FOREGROUND_* and BACKGROUND_* color attributes have same bit pattern. + // Constants below can be used in place of FOREGROUND values, and shift the value + // left by four bits to get BACKGROUNDs. + static BLUE: WORD = 1; + static GREEN: WORD = 2; + static RED: WORD = 4; + static INTENSITY: WORD = 8; + + static CLEAR_FG_COLOR_BITS_MASK: WORD = 0xfff0; + static CLEAR_BG_COLOR_BITS_MASK: WORD = 0xff0f; + + type SHORT = i16; + struct SMALL_RECT { + Left: SHORT, + Top: SHORT, + Right: SHORT, + Bottom: SHORT + } + struct COORD { + X: SHORT, + Y: SHORT + } + struct CONSOLE_SCREEN_BUFFER_INFO { + dwSize: COORD, + dwCursorPosition: COORD, + wAttributes: WORD, + srWindow: SMALL_RECT, + dwMaximumWindowSize: COORD + } + type PCONSOLE_SCREEN_BUFFER_INFO = *mut CONSOLE_SCREEN_BUFFER_INFO; + + extern "system" { + fn GetConsoleScreenBufferInfo( + hConsoleOutput: HANDLE, + lpConsoleScreenBufferInfo: PCONSOLE_SCREEN_BUFFER_INFO + ) -> BOOL; + fn SetConsoleTextAttribute(hConsoleOutput: HANDLE, + wAttributes: WORD) -> BOOL; + } + + fn get_console_attr(console: HANDLE) -> Option { + let mut info = CONSOLE_SCREEN_BUFFER_INFO { + dwSize: COORD { X: 0, Y: 0 }, + dwCursorPosition: COORD { X: 0, Y: 0 }, + wAttributes: 0, + srWindow: SMALL_RECT { Left: 0, Top: 0, Right: 0, Bottom: 0 }, + dwMaximumWindowSize: COORD { X: 0, Y: 0 } + }; + if unsafe { GetConsoleScreenBufferInfo(console, &mut info) } != 0 { + Some(info.wAttributes) + } else { + None + } + } + + fn set_console_attr(console: HANDLE, value: WORD) -> bool { + unsafe { SetConsoleTextAttribute(console, value) != 0 } + } + + fn color_to_console_attr(color: color::Color) -> WORD { + match color { + color::BLACK => 0, + color::RED => RED, + color::GREEN => GREEN, + color::YELLOW => RED | GREEN, + color::BLUE => BLUE, + color::MAGENTA => RED | BLUE, + color::CYAN => GREEN | BLUE, + color::WHITE => RED | GREEN | BLUE, + + color::BRIGHT_BLACK => INTENSITY, + color::BRIGHT_RED => RED | INTENSITY, + color::BRIGHT_GREEN => GREEN | INTENSITY, + color::BRIGHT_YELLOW => RED | GREEN | INTENSITY, + color::BRIGHT_BLUE => BLUE | INTENSITY, + color::BRIGHT_MAGENTA => RED | BLUE | INTENSITY, + color::BRIGHT_CYAN => GREEN | BLUE | INTENSITY, + color::BRIGHT_WHITE => RED | GREEN | BLUE | INTENSITY, + + _ => unreachable!() + } + } + + fn open_console() -> Option { + let conout = as_utf16_p("CONOUT$", |p| unsafe { + CreateFileW(p, GENERIC_READ | GENERIC_WRITE, 0, ptr::mut_null(), + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, ptr::mut_null()) + }); + if conout as uint == INVALID_HANDLE_VALUE as uint { + return None + } + Some(conout) + } + + pub fn new_console(out: T) -> Result<~TerminalOps:Send, ~str> { + let conout = match open_console() { + Some(c) => c, + None => return Err("Cannot open Windows console".to_owned()) + }; + let orig_attr = match get_console_attr(conout) { + Some(value) => value, + None => return Err("Couldn't get the current screen buffer attribute.".to_owned()) + }; + let console = WindowsConsole {conout: conout, out: out, orig_attr: orig_attr}; + Ok(box console as ~TerminalOps:Send) + } + + impl TerminalOps for WindowsConsole { + fn fg(&mut self, color: color::Color) -> io::IoResult { + let color = color_to_console_attr(color); + let console_attr = match get_console_attr(self.conout) { + Some(attr) => attr, + None => return Ok(false) + }; + Ok(set_console_attr(self.conout, console_attr & CLEAR_FG_COLOR_BITS_MASK | color)) + } + + fn bg(&mut self, color: color::Color) -> io::IoResult { + // shift left to get background color + let color = color_to_console_attr(color) << 4; + let console_attr = match get_console_attr(self.conout) { + Some(attr) => attr, + None => return Ok(false) + }; + Ok(set_console_attr(self.conout, console_attr & CLEAR_BG_COLOR_BITS_MASK | color)) + } + + fn attr(&mut self, attr: attr::Attr) -> io::IoResult { + match attr { + attr::ForegroundColor(c) => self.fg(c), + attr::BackgroundColor(c) => self.bg(c), + _ => Ok(false) + } + } + + fn supports_attr(&self, attr: attr::Attr) -> bool { + match attr { + attr::ForegroundColor(_) | attr::BackgroundColor(_) => true, + _ => false + } + } + + fn reset(&mut self) -> io::IoResult<()> { + set_console_attr(self.conout, self.orig_attr); + Ok(()) + } + + fn get_ref<'a>(&'a self) -> &'a T { &self.out } + + fn get_mut<'a>(&'a mut self) -> &'a mut T { &mut self.out } + } + + impl Writer for WindowsConsole { + fn write(&mut self, buf: &[u8]) -> io::IoResult<()> { + // We need to flush immediately after write call because Windows + // console maintains colors and attributes as a state on API, + // unlike TTYs, which encode these in characters. + // We can avoid this by manually keep track of states and writes, + // although it doesn't seem to worth complexity. + self.out.write(buf).and_then(|_| self.out.flush()) + } + + fn flush(&mut self) -> io::IoResult<()> { + self.out.flush() + } + } + + #[unsafe_destructor] + impl Drop for WindowsConsole { + fn drop(&mut self) { + unsafe { + CloseHandle(self.conout); + } + } + } + + #[cfg(test)] + #[test] + fn test_windows_console() { + let conout = match open_console() { + Some(c) => c, + None => return () + }; + let orig_attr = get_console_attr(conout).unwrap(); + let mut t = new_terminal(io::stdout()).unwrap(); + + t.fg(color::MAGENTA).unwrap(); + assert_eq!(get_console_attr(conout).unwrap() & 0xf, RED | BLUE); + + t.bg(color::CYAN).unwrap(); + assert_eq!(get_console_attr(conout).unwrap() & 0xff, + RED | BLUE | (GREEN | BLUE) << 4); + + t.fg(color::YELLOW).unwrap(); + assert_eq!(get_console_attr(conout).unwrap() & 0xff, + RED | GREEN | (GREEN | BLUE) << 4); + + t.reset().unwrap(); + assert_eq!(get_console_attr(conout).unwrap(), orig_attr); + + assert!(t.supports_attr(attr::ForegroundColor(color::RED))); + assert!(t.supports_attr(attr::BackgroundColor(color::BLUE))); + assert!(!t.supports_attr(attr::Bold)); + + t.attr(attr::ForegroundColor(color::RED)).unwrap(); + assert_eq!(get_console_attr(conout).unwrap() & 0xf, RED); + t.attr(attr::BackgroundColor(color::BLUE)).unwrap(); + assert_eq!(get_console_attr(conout).unwrap() & 0xff, RED | BLUE << 4); + + t.reset().unwrap(); + } +} diff --git a/src/libtest/lib.rs b/src/libtest/lib.rs index fa55444fe5b27..78c1eaf71ebc0 100644 --- a/src/libtest/lib.rs +++ b/src/libtest/lib.rs @@ -47,7 +47,6 @@ use time::precise_time_ns; use getopts::{OptGroup, optflag, optopt}; use serialize::{json, Decodable}; use serialize::json::{Json, ToJson}; -use term::Terminal; use term::color::{Color, RED, YELLOW, GREEN, CYAN}; use std::cmp; @@ -427,7 +426,7 @@ pub enum TestResult { } enum OutputLocation { - Pretty(term::Terminal), + Pretty(~term::TerminalOps), Raw(T), } @@ -452,7 +451,7 @@ impl ConsoleTestState { Some(ref path) => Some(try!(File::create(path))), None => None }; - let out = match term::Terminal::new(io::stdio::stdout_raw()) { + let out = match term::new_terminal(io::stdio::stdout_raw()) { Err(_) => Raw(io::stdio::stdout_raw()), Ok(t) => Pretty(t) };