From e8a30cb8934106e63f9d0938c547e535acb7b888 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sun, 20 Nov 2016 11:14:52 -0500 Subject: [PATCH] Completely re-work colored output and tty handling. This commit completely guts all of the color handling code and replaces most of it with two new crates: wincolor and termcolor. wincolor provides a simple API to coloring using the Windows console and termcolor provides a platform independent coloring API tuned for multithreaded command line programs. This required a lot more flexibility than what the `term` crate provided, so it was dropped. We instead switch to writing ANSI escape sequences directly and ignore the TERMINFO database. In addition to fixing several bugs, this commit also permits end users to customize colors to a certain extent. For example, this command will set the match color to magenta and the line number background to yellow: rg --colors 'match:fg:magenta' --colors 'line:bg:yellow' foo For tty handling, we've adopted a hack from `git` to do tty detection in MSYS/mintty terminals. As a result, ripgrep should get both color detection and piping correct on Windows regardless of which terminal you use. Finally, switch to line buffering. Performance doesn't seem to be impacted and it's an otherwise more user friendly option. Fixes #37, Fixes #51, Fixes #94, Fixes #117, Fixes #182, Fixes #231 --- .gitignore | 2 + Cargo.lock | 35 +- Cargo.toml | 6 +- appveyor.yml | 2 + ci/script.sh | 2 + doc/rg.1 | 22 + doc/rg.1.md | 16 + src/app.rs | 24 +- src/args.rs | 105 +++-- src/atty.rs | 111 ++++- src/main.rs | 60 +-- src/out.rs | 374 --------------- src/printer.rs | 437 ++++++++++++++--- src/search_buffer.rs | 12 +- src/search_stream.rs | 14 +- src/terminal_win.rs | 176 ------- src/worker.rs | 8 +- termcolor/Cargo.toml | 20 + termcolor/README.md | 88 ++++ termcolor/src/lib.rs | 1071 ++++++++++++++++++++++++++++++++++++++++++ wincolor/Cargo.toml | 21 + wincolor/README.md | 44 ++ wincolor/src/lib.rs | 242 ++++++++++ 23 files changed, 2153 insertions(+), 739 deletions(-) delete mode 100644 src/out.rs delete mode 100644 src/terminal_win.rs create mode 100644 termcolor/Cargo.toml create mode 100644 termcolor/README.md create mode 100644 termcolor/src/lib.rs create mode 100644 wincolor/Cargo.toml create mode 100644 wincolor/README.md create mode 100644 wincolor/src/lib.rs diff --git a/.gitignore b/.gitignore index d2fafa58a..be83b91c5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ target /grep/Cargo.lock /globset/Cargo.lock /ignore/Cargo.lock +/termcolor/Cargo.lock +/wincolor/Cargo.lock diff --git a/Cargo.lock b/Cargo.lock index ada226fb1..df98857d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,14 +9,14 @@ dependencies = [ "grep 0.1.4", "ignore 0.1.5", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", "memmap 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)", - "term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 0.1.0", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -106,7 +106,7 @@ version = "0.1.2" dependencies = [ "aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)", @@ -129,7 +129,7 @@ version = "0.1.5" dependencies = [ "crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", "globset 0.1.2", - "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)", @@ -148,7 +148,7 @@ dependencies = [ [[package]] name = "lazy_static" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -217,22 +217,20 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "term" -version = "0.4.4" +name = "term_size" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "term_size" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" +name = "termcolor" +version = "0.1.0" dependencies = [ - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "wincolor 0.1.0", ] [[package]] @@ -322,6 +320,14 @@ name = "winapi-build" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "wincolor" +version = "0.1.0" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [metadata] "checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66" "checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6" @@ -334,7 +340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc484842f1e2884faf56f529f960cc12ad8c71ce96cc7abba0a067c98fee344" "checksum fs2 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "640001e1bd865c7c32806292822445af576a6866175b5225aa2087ca5e3de551" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -"checksum lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "49247ec2a285bb3dcb23cbd9c35193c025e7251bfce77c1d5da97e6362dffe7f" +"checksum lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6abe0ee2e758cd6bc8a2cd56726359007748fbf4128da998b65d0b70f881e19b" "checksum libc 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)" = "044d1360593a78f5c8e5e710beccdc24ab71d1f01bc19a29bcacdba22e8475d8" "checksum log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ab83497bf8bf4ed2a74259c1c802351fcd67a65baa86394b6ba73c36f4838054" "checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" @@ -344,7 +350,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957" "checksum simd 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "63b5847c2d766ca7ce7227672850955802fabd779ba616aeabead4c2c3877023" "checksum strsim 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "50c069df92e4b01425a8bf3576d5d417943a6a7272fbabaf5bd80b1aaa76442e" -"checksum term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3deff8a2b3b6607d6d7cc32ac25c0b33709453ca9cceac006caac51e963cf94a" "checksum term_size 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3f7f5f3f71b0040cecc71af239414c23fd3c73570f5ff54cf50e03cef637f2a0" "checksum thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03" "checksum thread-id 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4437c97558c70d129e40629a5b385b3fb1ffac301e63941335e4d354081ec14a" diff --git a/Cargo.toml b/Cargo.toml index afed8439f..b75170305 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,11 +38,11 @@ memchr = "0.1" memmap = "0.5" num_cpus = "1" regex = "0.1.77" -term = "0.4" +termcolor = { version = "0.1.0", path = "termcolor" } [target.'cfg(windows)'.dependencies] -kernel32-sys = "0.2" -winapi = "0.2" +kernel32-sys = "0.2.2" +winapi = "0.2.8" [build-dependencies] clap = "2.18" diff --git a/appveyor.yml b/appveyor.yml index 800bc947e..c089e07b6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -31,6 +31,8 @@ test_script: - cargo test --verbose --manifest-path grep/Cargo.toml - cargo test --verbose --manifest-path globset/Cargo.toml - cargo test --verbose --manifest-path ignore/Cargo.toml + - cargo test --verbose --manifest-path wincolor/Cargo.toml + - cargo test --verbose --manifest-path termcolor/Cargo.toml before_deploy: # Generate artifacts for release diff --git a/ci/script.sh b/ci/script.sh index bf0731a2b..ccda56f57 100644 --- a/ci/script.sh +++ b/ci/script.sh @@ -25,6 +25,8 @@ run_test_suite() { cargo test --target $TARGET --verbose --manifest-path globset/Cargo.toml cargo build --target $TARGET --verbose --manifest-path ignore/Cargo.toml cargo test --target $TARGET --verbose --manifest-path ignore/Cargo.toml + cargo build --target $TARGET --verbose --manifest-path termcolor/Cargo.toml + cargo test --target $TARGET --verbose --manifest-path termcolor/Cargo.toml # sanity check the file type file target/$TARGET/debug/rg diff --git a/doc/rg.1 b/doc/rg.1 index d07909555..bf49ec57e 100644 --- a/doc/rg.1 +++ b/doc/rg.1 @@ -143,6 +143,28 @@ Show NUM lines before and after each match. .RS .RE .TP +.B \-\-colors \f[I]SPEC\f[] ... +This flag specifies color settings for use in the output. +This flag may be provided multiple times. +Settings are applied iteratively. +Colors are limited to one of eight choices: red, blue, green, cyan, +magenta, yellow, white and black. +Styles are limited to either nobold or bold. +.RS +.PP +The format of the flag is {type}:{attribute}:{value}. +{type} should be one of path, line or match. +{attribute} can be fg, bg or style. +Value is either a color (for fg and bg) or a text style. +A special format, {type}:none, will clear all color settings for {type}. +.PP +For example, the following command will change the match color to +magenta and the background color for line numbers to yellow: +.PP +rg \-\-colors \[aq]match:fg:magenta\[aq] \-\-colors +\[aq]line:bg:yellow\[aq] foo. +.RE +.TP .B \-\-column Show column numbers (1 based) in output. This only shows the column numbers for the first match on each line. diff --git a/doc/rg.1.md b/doc/rg.1.md index 8e6226d7c..11487d69b 100644 --- a/doc/rg.1.md +++ b/doc/rg.1.md @@ -95,6 +95,22 @@ Project home page: https://github.com/BurntSushi/ripgrep -C, --context *NUM* : Show NUM lines before and after each match. +--colors *SPEC* ... +: This flag specifies color settings for use in the output. This flag may be + provided multiple times. Settings are applied iteratively. Colors are limited + to one of eight choices: red, blue, green, cyan, magenta, yellow, white and + black. Styles are limited to either nobold or bold. + + The format of the flag is {type}:{attribute}:{value}. {type} should be one + of path, line or match. {attribute} can be fg, bg or style. Value is either + a color (for fg and bg) or a text style. A special format, {type}:none, + will clear all color settings for {type}. + + For example, the following command will change the match color to magenta + and the background color for line numbers to yellow: + + rg --colors 'match:fg:magenta' --colors 'line:bg:yellow' foo. + --column : Show column numbers (1 based) in output. This only shows the column numbers for the first match on each line. Note that this doesn't try diff --git a/src/app.rs b/src/app.rs index 33dfb2715..231f6bcb2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -88,7 +88,9 @@ fn app(next_line_help: bool, doc: F) -> App<'static, 'static> .value_name("WHEN") .takes_value(true) .hide_possible_values(true) - .possible_values(&["never", "always", "auto"])) + .possible_values(&["never", "auto", "always", "ansi"])) + .arg(flag("colors").value_name("SPEC") + .takes_value(true).multiple(true).number_of_values(1)) .arg(flag("fixed-strings").short("F")) .arg(flag("glob").short("g") .takes_value(true).multiple(true).number_of_values(1) @@ -220,7 +222,25 @@ lazy_static! { doc!(h, "color", "When to use color. [default: auto]", "When to use color in the output. The possible values are \ - never, always or auto. The default is auto."); + never, auto, always or ansi. The default is auto. When always \ + is used, coloring is attempted based on your environment. When \ + ansi used, coloring is forcefully done using ANSI escape color \ + codes."); + doc!(h, "colors", + "Configure color settings and styles.", + "This flag specifies color settings for use in the output. \ + This flag may be provided multiple times. Settings are applied \ + iteratively. Colors are limited to one of eight choices: \ + red, blue, green, cyan, magenta, yellow, white and black. \ + Styles are limited to either nobold or bold.\n\nThe format \ + of the flag is {type}:{attribute}:{value}. {type} should be \ + one of path, line or match. {attribute} can be fg, bg or style. \ + {value} is either a color (for fg and bg) or a text style. \ + A special format, {type}:none, will clear all color settings \ + for {type}.\n\nFor example, the following command will change \ + the match color to magenta and the background color for line \ + numbers to yellow:\n\n\ + rg --colors 'match:fg:magenta' --colors 'line:bg:yellow' foo."); doc!(h, "fixed-strings", "Treat the pattern as a literal string.", "Treat the pattern as a literal string instead of a regular \ diff --git a/src/args.rs b/src/args.rs index 836b28194..a98a1b6a2 100644 --- a/src/args.rs +++ b/src/args.rs @@ -13,21 +13,14 @@ use grep::{Grep, GrepBuilder}; use log; use num_cpus; use regex; -use term::Terminal; -#[cfg(not(windows))] -use term; -#[cfg(windows)] -use term::WinConsole; +use termcolor; -use atty; use app; +use atty; use ignore::overrides::{Override, OverrideBuilder}; use ignore::types::{FileTypeDef, Types, TypesBuilder}; use ignore; -use out::{Out, ColoredTerminal}; -use printer::Printer; -#[cfg(windows)] -use terminal_win::WindowsBuffer; +use printer::{ColorSpecs, Printer}; use unescape::unescape; use worker::{Worker, WorkerBuilder}; @@ -40,6 +33,8 @@ pub struct Args { after_context: usize, before_context: usize, color: bool, + color_choice: termcolor::ColorChoice, + colors: ColorSpecs, column: bool, context_separator: Vec, count: bool, @@ -132,8 +127,9 @@ impl Args { /// Create a new printer of individual search results that writes to the /// writer given. - pub fn printer(&self, wtr: W) -> Printer { + pub fn printer(&self, wtr: W) -> Printer { let mut p = Printer::new(wtr) + .colors(self.colors.clone()) .column(self.column) .context_separator(self.context_separator.clone()) .eol(self.eol) @@ -147,16 +143,6 @@ impl Args { p } - /// Create a new printer of search results for an entire file that writes - /// to the writer given. - pub fn out(&self) -> Out { - let mut out = Out::new(self.color); - if let Some(filesep) = self.file_separator() { - out = out.file_separator(filesep); - } - out - } - /// Retrieve the configured file separator. pub fn file_separator(&self) -> Option> { if self.heading && !self.count && !self.files_with_matches && !self.files_without_matches { @@ -173,30 +159,17 @@ impl Args { self.max_count == Some(0) } - /// Create a new buffer for use with searching. - #[cfg(not(windows))] - pub fn outbuf(&self) -> ColoredTerminal>> { - ColoredTerminal::new(vec![], self.color) - } - - /// Create a new buffer for use with searching. - #[cfg(windows)] - pub fn outbuf(&self) -> ColoredTerminal { - ColoredTerminal::new_buffer(self.color) - } - - /// Create a new buffer for use with searching. - #[cfg(not(windows))] - pub fn stdout( - &self, - ) -> ColoredTerminal>> { - ColoredTerminal::new(io::BufWriter::new(io::stdout()), self.color) + /// Create a new writer for single-threaded searching with color support. + pub fn stdout(&self) -> termcolor::Stdout { + termcolor::Stdout::new(self.color_choice) } - /// Create a new buffer for use with searching. - #[cfg(windows)] - pub fn stdout(&self) -> ColoredTerminal> { - ColoredTerminal::new_stdout(self.color) + /// Create a new buffer writer for multi-threaded searching with color + /// support. + pub fn buffer_writer(&self) -> termcolor::BufferWriter { + let mut wtr = termcolor::BufferWriter::stdout(self.color_choice); + wtr.separator(self.file_separator()); + wtr } /// Return the paths that should be searched. @@ -312,6 +285,8 @@ impl<'a> ArgMatches<'a> { after_context: after_context, before_context: before_context, color: self.color(), + color_choice: self.color_choice(), + colors: try!(self.color_specs()), column: self.column(), context_separator: self.context_separator(), count: self.is_present("count"), @@ -617,6 +592,50 @@ impl<'a> ArgMatches<'a> { } } + /// Returns the user's color choice based on command line parameters and + /// environment. + fn color_choice(&self) -> termcolor::ColorChoice { + let preference = match self.0.value_of_lossy("color") { + None => "auto".to_string(), + Some(v) => v.into_owned(), + }; + if preference == "always" { + termcolor::ColorChoice::Always + } else if preference == "ansi" { + termcolor::ColorChoice::AlwaysAnsi + } else if self.is_present("vimgrep") { + termcolor::ColorChoice::Never + } else if preference == "auto" { + if atty::on_stdout() || self.is_present("pretty") { + termcolor::ColorChoice::Auto + } else { + termcolor::ColorChoice::Never + } + } else { + termcolor::ColorChoice::Never + } + } + + /// Returns the color specifications given by the user on the CLI. + /// + /// If the was a problem parsing any of the provided specs, then an error + /// is returned. + fn color_specs(&self) -> Result { + // Start with a default set of color specs. + let mut specs = vec![ + "path:fg:green".parse().unwrap(), + "path:style:bold".parse().unwrap(), + "line:fg:blue".parse().unwrap(), + "line:style:bold".parse().unwrap(), + "match:fg:red".parse().unwrap(), + "match:style:bold".parse().unwrap(), + ]; + for spec_str in self.values_of_lossy_vec("colors") { + specs.push(try!(spec_str.parse())); + } + Ok(ColorSpecs::new(&specs)) + } + /// Returns the approximate number of threads that ripgrep should use. fn threads(&self) -> Result { let threads = try!(self.usize_of("threads")).unwrap_or(0); diff --git a/src/atty.rs b/src/atty.rs index 978c3749d..9e96fe6e6 100644 --- a/src/atty.rs +++ b/src/atty.rs @@ -4,6 +4,11 @@ from (or to) a terminal. Windows and Unix do this differently, so implement both here. */ +#[cfg(windows)] +use winapi::minwindef::DWORD; +#[cfg(windows)] +use winapi::winnt::HANDLE; + #[cfg(unix)] pub fn stdin_is_readable() -> bool { use std::fs::File; @@ -44,26 +49,104 @@ pub fn on_stdout() -> bool { /// Returns true if there is a tty on stdin. #[cfg(windows)] pub fn on_stdin() -> bool { - // BUG: https://github.com/BurntSushi/ripgrep/issues/19 - // It's not clear to me how to determine whether there is a tty on stdin. - // Checking GetConsoleMode(GetStdHandle(stdin)) != 0 appears to report - // that stdin is a pipe, even if it's not in a cygwin terminal, for - // example. - // - // To fix this, we just assume there is always a tty on stdin. If Windows - // users need to search stdin, they'll have to pass -. Ug. - true + use kernel32::GetStdHandle; + use winapi::winbase::{ + STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE, + }; + + unsafe { + let stdin = GetStdHandle(STD_INPUT_HANDLE); + if console_on_handle(stdin) { + // False positives aren't possible. If we got a console then + // we definitely have a tty on stdin. + return true; + } + // Otherwise, it's possible to get a false negative. If we know that + // there's a console on stdout or stderr however, then this is a true + // negative. + if console_on_fd(STD_OUTPUT_HANDLE) + || console_on_fd(STD_ERROR_HANDLE) { + return false; + } + // Otherwise, we can't really tell, so we do a weird hack. + msys_tty_on_handle(stdin) + } } /// Returns true if there is a tty on stdout. #[cfg(windows)] pub fn on_stdout() -> bool { - use kernel32; - use winapi; + use kernel32::GetStdHandle; + use winapi::winbase::{ + STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE, + }; + + unsafe { + let stdout = GetStdHandle(STD_OUTPUT_HANDLE); + if console_on_handle(stdout) { + // False positives aren't possible. If we got a console then + // we definitely have a tty on stdout. + return true; + } + // Otherwise, it's possible to get a false negative. If we know that + // there's a console on stdin or stderr however, then this is a true + // negative. + if console_on_fd(STD_INPUT_HANDLE) || console_on_fd(STD_ERROR_HANDLE) { + return false; + } + // Otherwise, we can't really tell, so we do a weird hack. + msys_tty_on_handle(stdout) + } +} + +/// Returns true if there is an MSYS tty on the given handle. +#[cfg(windows)] +fn msys_tty_on_handle(handle: HANDLE) -> bool { + use std::ffi::OsString; + use std::mem; + use std::os::raw::c_void; + use std::os::windows::ffi::OsStringExt; + use std::slice; + + use kernel32::{GetFileInformationByHandleEx}; + use winapi::fileapi::FILE_NAME_INFO; + use winapi::minwinbase::FileNameInfo; + use winapi::minwindef::MAX_PATH; unsafe { - let fd = winapi::winbase::STD_OUTPUT_HANDLE; - let mut out = 0; - kernel32::GetConsoleMode(kernel32::GetStdHandle(fd), &mut out) != 0 + let size = mem::size_of::(); + let mut name_info_bytes = vec![0u8; size + MAX_PATH]; + let res = GetFileInformationByHandleEx( + handle, + FileNameInfo, + &mut *name_info_bytes as *mut _ as *mut c_void, + name_info_bytes.len() as u32); + if res == 0 { + return true; + } + let name_info: FILE_NAME_INFO = + *(name_info_bytes[0..size].as_ptr() as *const FILE_NAME_INFO); + let name_bytes = + &name_info_bytes[size..size + name_info.FileNameLength as usize]; + let name_u16 = slice::from_raw_parts( + name_bytes.as_ptr() as *const u16, name_bytes.len() / 2); + let name = OsString::from_wide(name_u16) + .as_os_str().to_string_lossy().into_owned(); + name.contains("msys-") || name.contains("-pty") } } + +/// Returns true if there is a console on the given file descriptor. +#[cfg(windows)] +unsafe fn console_on_fd(fd: DWORD) -> bool { + use kernel32::GetStdHandle; + console_on_handle(GetStdHandle(fd)) +} + +/// Returns true if there is a console on the given handle. +#[cfg(windows)] +fn console_on_handle(handle: HANDLE) -> bool { + use kernel32::GetConsoleMode; + let mut out = 0; + unsafe { GetConsoleMode(handle, &mut out) != 0 } +} diff --git a/src/main.rs b/src/main.rs index 0eecb13b9..2bf1cf027 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ extern crate memchr; extern crate memmap; extern crate num_cpus; extern crate regex; -extern crate term; +extern crate termcolor; #[cfg(windows)] extern crate winapi; @@ -31,9 +31,8 @@ use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::mpsc; use std::thread; -use std::cmp; -use term::Terminal; +use termcolor::WriteColor; use args::Args; use worker::Work; @@ -54,13 +53,10 @@ macro_rules! eprintln { mod app; mod args; mod atty; -mod out; mod pathutil; mod printer; mod search_buffer; mod search_stream; -#[cfg(windows)] -mod terminal_win; mod unescape; mod worker; @@ -84,16 +80,13 @@ fn run(args: Arc) -> Result { { let args = args.clone(); ctrlc::set_handler(move || { - let stdout = io::stdout(); - let mut stdout = stdout.lock(); - - let _ = args.stdout().reset(); - let _ = stdout.flush(); - + let mut writer = args.stdout(); + let _ = writer.reset(); + let _ = writer.flush(); process::exit(1); }); } - let threads = cmp::max(1, args.threads() - 1); + let threads = args.threads(); if args.files() { if threads == 1 || args.is_one_path() { run_files_one_thread(args) @@ -110,7 +103,7 @@ fn run(args: Arc) -> Result { } fn run_parallel(args: Arc) -> Result { - let out = Arc::new(Mutex::new(args.out())); + let bufwtr = Arc::new(args.buffer_writer()); let quiet_matched = QuietMatched::new(args.quiet()); let paths_searched = Arc::new(AtomicUsize::new(0)); let match_count = Arc::new(AtomicUsize::new(0)); @@ -120,8 +113,8 @@ fn run_parallel(args: Arc) -> Result { let quiet_matched = quiet_matched.clone(); let paths_searched = paths_searched.clone(); let match_count = match_count.clone(); - let out = out.clone(); - let mut outbuf = args.outbuf(); + let bufwtr = bufwtr.clone(); + let mut buf = bufwtr.buffer(); let mut worker = args.worker(); Box::new(move |result| { use ignore::WalkState::*; @@ -134,11 +127,11 @@ fn run_parallel(args: Arc) -> Result { Some(dent) => dent, }; paths_searched.fetch_add(1, Ordering::SeqCst); - outbuf.clear(); + buf.clear(); { // This block actually executes the search and prints the // results into outbuf. - let mut printer = args.printer(&mut outbuf); + let mut printer = args.printer(&mut buf); let count = if dent.is_stdin() { worker.run(&mut printer, Work::Stdin) @@ -150,17 +143,9 @@ fn run_parallel(args: Arc) -> Result { return Quit; } } - if !outbuf.get_ref().is_empty() { - // This should be the only mutex in all of ripgrep. Since the - // common case is to report a small number of matches relative - // to the corpus, this really shouldn't matter much. - // - // Still, it'd be nice to send this on a channel, but then we'd - // need to manage a pool of outbufs, which would complicate the - // code. - let mut out = out.lock().unwrap(); - out.write(&outbuf); - } + // BUG(burntsushi): We should handle this error instead of ignoring + // it. See: https://github.com/BurntSushi/ripgrep/issues/200 + let _ = bufwtr.print(&buf); Continue }) }); @@ -173,8 +158,9 @@ fn run_parallel(args: Arc) -> Result { } fn run_one_thread(args: Arc) -> Result { + let stdout = args.stdout(); + let mut stdout = stdout.lock(); let mut worker = args.worker(); - let mut term = args.stdout(); let mut paths_searched: u64 = 0; let mut match_count = 0; for result in args.walker() { @@ -182,7 +168,7 @@ fn run_one_thread(args: Arc) -> Result { None => continue, Some(dent) => dent, }; - let mut printer = args.printer(&mut term); + let mut printer = args.printer(&mut stdout); if match_count > 0 { if args.quiet() { break; @@ -211,8 +197,8 @@ fn run_files_parallel(args: Arc) -> Result { let print_args = args.clone(); let (tx, rx) = mpsc::channel::(); let print_thread = thread::spawn(move || { - let term = print_args.stdout(); - let mut printer = print_args.printer(term); + let stdout = print_args.stdout(); + let mut printer = print_args.printer(stdout.lock()); let mut file_count = 0; for dent in rx.iter() { printer.path(dent.path()); @@ -234,8 +220,8 @@ fn run_files_parallel(args: Arc) -> Result { } fn run_files_one_thread(args: Arc) -> Result { - let term = args.stdout(); - let mut printer = args.printer(term); + let stdout = args.stdout(); + let mut printer = args.printer(stdout.lock()); let mut file_count = 0; for result in args.walker() { let dent = match get_or_log_dir_entry(result, args.no_messages()) { @@ -249,8 +235,8 @@ fn run_files_one_thread(args: Arc) -> Result { } fn run_types(args: Arc) -> Result { - let term = args.stdout(); - let mut printer = args.printer(term); + let stdout = args.stdout(); + let mut printer = args.printer(stdout.lock()); let mut ty_count = 0; for def in args.type_defs() { printer.type_def(def); diff --git a/src/out.rs b/src/out.rs deleted file mode 100644 index 389f54588..000000000 --- a/src/out.rs +++ /dev/null @@ -1,374 +0,0 @@ -use std::io::{self, Write}; - -use term::{self, Terminal}; -#[cfg(not(windows))] -use term::terminfo::TermInfo; -#[cfg(windows)] -use term::WinConsole; - -#[cfg(windows)] -use terminal_win::WindowsBuffer; - -/// Out controls the actual output of all search results for a particular file -/// to the end user. -/// -/// (The difference between Out and Printer is that a Printer works with -/// individual search results where as Out works with search results for each -/// file as a whole. For example, it knows when to print a file separator.) -pub struct Out { - #[cfg(not(windows))] - term: ColoredTerminal>>, - #[cfg(windows)] - term: ColoredTerminal>, - printed: bool, - file_separator: Option>, -} - -impl Out { - /// Create a new Out that writes to the wtr given. - #[cfg(not(windows))] - pub fn new(color: bool) -> Out { - let wtr = io::BufWriter::new(io::stdout()); - Out { - term: ColoredTerminal::new(wtr, color), - printed: false, - file_separator: None, - } - } - - /// Create a new Out that writes to the wtr given. - #[cfg(windows)] - pub fn new(color: bool) -> Out { - Out { - term: ColoredTerminal::new_stdout(color), - printed: false, - file_separator: None, - } - } - - /// If set, the separator is printed between matches from different files. - /// By default, no separator is printed. - pub fn file_separator(mut self, sep: Vec) -> Out { - self.file_separator = Some(sep); - self - } - - /// Write the search results of a single file to the underlying wtr and - /// flush wtr. - #[cfg(not(windows))] - pub fn write( - &mut self, - buf: &ColoredTerminal>>, - ) { - self.write_sep(); - match *buf { - ColoredTerminal::Colored(ref tt) => { - let _ = self.term.write_all(tt.get_ref()); - } - ColoredTerminal::NoColor(ref buf) => { - let _ = self.term.write_all(buf); - } - } - self.write_done(); - } - /// Write the search results of a single file to the underlying wtr and - /// flush wtr. - #[cfg(windows)] - pub fn write( - &mut self, - buf: &ColoredTerminal, - ) { - self.write_sep(); - match *buf { - ColoredTerminal::Colored(ref tt) => { - tt.print_stdout(&mut self.term); - } - ColoredTerminal::NoColor(ref buf) => { - let _ = self.term.write_all(buf); - } - } - self.write_done(); - } - - fn write_sep(&mut self) { - if let Some(ref sep) = self.file_separator { - if self.printed { - let _ = self.term.write_all(sep); - let _ = self.term.write_all(b"\n"); - } - } - } - - fn write_done(&mut self) { - let _ = self.term.flush(); - self.printed = true; - } -} - -/// ColoredTerminal provides optional colored output through the term::Terminal -/// trait. In particular, it will dynamically configure itself to use coloring -/// if it's available in the environment. -#[derive(Clone, Debug)] -pub enum ColoredTerminal { - Colored(T), - NoColor(T::Output), -} - -#[cfg(not(windows))] -impl ColoredTerminal> { - /// Create a new output buffer. - /// - /// When color is true, the buffer will attempt to support coloring. - pub fn new(wtr: W, color: bool) -> Self { - lazy_static! { - // Only pay for parsing the terminfo once. - static ref TERMINFO: Option = { - match TermInfo::from_env() { - Ok(info) => Some(info), - Err(err) => { - debug!("error loading terminfo for coloring: {}", err); - None - } - } - }; - } - // If we want color, build a term::TerminfoTerminal and see if the - // current environment supports coloring. If not, bail with NoColor. To - // avoid losing our writer (ownership), do this the long way. - if !color { - return ColoredTerminal::NoColor(wtr); - } - let terminfo = match *TERMINFO { - None => return ColoredTerminal::NoColor(wtr), - Some(ref ti) => { - // Ug, this should go away with the next release of `term`. - TermInfo { - names: ti.names.clone(), - bools: ti.bools.clone(), - numbers: ti.numbers.clone(), - strings: ti.strings.clone(), - } - } - }; - let tt = term::TerminfoTerminal::new_with_terminfo(wtr, terminfo); - if !tt.supports_color() { - debug!("environment doesn't support coloring"); - return ColoredTerminal::NoColor(tt.into_inner()); - } - ColoredTerminal::Colored(tt) - } -} - -#[cfg(not(windows))] -impl ColoredTerminal>> { - /// Clear the give buffer of all search results such that it is reusable - /// in another search. - pub fn clear(&mut self) { - match *self { - ColoredTerminal::Colored(ref mut tt) => { - tt.get_mut().clear(); - } - ColoredTerminal::NoColor(ref mut buf) => { - buf.clear(); - } - } - } -} - -#[cfg(windows)] -impl ColoredTerminal { - /// Create a new output buffer. - /// - /// When color is true, the buffer will attempt to support coloring. - pub fn new_buffer(color: bool) -> Self { - if !color { - ColoredTerminal::NoColor(vec![]) - } else { - ColoredTerminal::Colored(WindowsBuffer::new()) - } - } - - /// Clear the give buffer of all search results such that it is reusable - /// in another search. - pub fn clear(&mut self) { - match *self { - ColoredTerminal::Colored(ref mut win) => win.clear(), - ColoredTerminal::NoColor(ref mut buf) => buf.clear(), - } - } -} - -#[cfg(windows)] -impl ColoredTerminal> { - /// Create a new output buffer. - /// - /// When color is true, the buffer will attempt to support coloring. - pub fn new_stdout(color: bool) -> Self { - if !color { - return ColoredTerminal::NoColor(io::stdout()); - } - match WinConsole::new(io::stdout()) { - Ok(win) => ColoredTerminal::Colored(win), - Err(_) => ColoredTerminal::NoColor(io::stdout()), - } - } -} - -impl ColoredTerminal { - fn map_result( - &mut self, - mut f: F, - ) -> term::Result<()> - where F: FnMut(&mut T) -> term::Result<()> { - match *self { - ColoredTerminal::Colored(ref mut w) => f(w), - ColoredTerminal::NoColor(_) => Err(term::Error::NotSupported), - } - } - - fn map_bool( - &self, - mut f: F, - ) -> bool - where F: FnMut(&T) -> bool { - match *self { - ColoredTerminal::Colored(ref w) => f(w), - ColoredTerminal::NoColor(_) => false, - } - } -} - -impl io::Write for ColoredTerminal { - fn write(&mut self, buf: &[u8]) -> io::Result { - match *self { - ColoredTerminal::Colored(ref mut w) => w.write(buf), - ColoredTerminal::NoColor(ref mut w) => w.write(buf), - } - } - - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -impl term::Terminal for ColoredTerminal { - type Output = T::Output; - - fn fg(&mut self, fg: term::color::Color) -> term::Result<()> { - self.map_result(|w| w.fg(fg)) - } - - fn bg(&mut self, bg: term::color::Color) -> term::Result<()> { - self.map_result(|w| w.bg(bg)) - } - - fn attr(&mut self, attr: term::Attr) -> term::Result<()> { - self.map_result(|w| w.attr(attr)) - } - - fn supports_attr(&self, attr: term::Attr) -> bool { - self.map_bool(|w| w.supports_attr(attr)) - } - - fn reset(&mut self) -> term::Result<()> { - self.map_result(|w| w.reset()) - } - - fn supports_reset(&self) -> bool { - self.map_bool(|w| w.supports_reset()) - } - - fn supports_color(&self) -> bool { - self.map_bool(|w| w.supports_color()) - } - - fn cursor_up(&mut self) -> term::Result<()> { - self.map_result(|w| w.cursor_up()) - } - - fn delete_line(&mut self) -> term::Result<()> { - self.map_result(|w| w.delete_line()) - } - - fn carriage_return(&mut self) -> term::Result<()> { - self.map_result(|w| w.carriage_return()) - } - - fn get_ref(&self) -> &Self::Output { - match *self { - ColoredTerminal::Colored(ref w) => w.get_ref(), - ColoredTerminal::NoColor(ref w) => w, - } - } - - fn get_mut(&mut self) -> &mut Self::Output { - match *self { - ColoredTerminal::Colored(ref mut w) => w.get_mut(), - ColoredTerminal::NoColor(ref mut w) => w, - } - } - - fn into_inner(self) -> Self::Output { - match self { - ColoredTerminal::Colored(w) => w.into_inner(), - ColoredTerminal::NoColor(w) => w, - } - } -} - -impl<'a, T: Terminal + Send> term::Terminal for &'a mut ColoredTerminal { - type Output = T::Output; - - fn fg(&mut self, fg: term::color::Color) -> term::Result<()> { - (**self).fg(fg) - } - - fn bg(&mut self, bg: term::color::Color) -> term::Result<()> { - (**self).bg(bg) - } - - fn attr(&mut self, attr: term::Attr) -> term::Result<()> { - (**self).attr(attr) - } - - fn supports_attr(&self, attr: term::Attr) -> bool { - (**self).supports_attr(attr) - } - - fn reset(&mut self) -> term::Result<()> { - (**self).reset() - } - - fn supports_reset(&self) -> bool { - (**self).supports_reset() - } - - fn supports_color(&self) -> bool { - (**self).supports_color() - } - - fn cursor_up(&mut self) -> term::Result<()> { - (**self).cursor_up() - } - - fn delete_line(&mut self) -> term::Result<()> { - (**self).delete_line() - } - - fn carriage_return(&mut self) -> term::Result<()> { - (**self).carriage_return() - } - - fn get_ref(&self) -> &Self::Output { - (**self).get_ref() - } - - fn get_mut(&mut self) -> &mut Self::Output { - (**self).get_mut() - } - - fn into_inner(self) -> Self::Output { - // Good golly miss molly... - unimplemented!() - } -} diff --git a/src/printer.rs b/src/printer.rs index 1b8e5965d..4c04c0eed 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -1,8 +1,10 @@ +use std::error; +use std::fmt; use std::path::Path; +use std::str::FromStr; use regex::bytes::Regex; -use term::{Attr, Terminal}; -use term::color; +use termcolor::{Color, ColorSpec, ParseColorError, WriteColor}; use pathutil::strip_prefix; use ignore::types::FileTypeDef; @@ -40,38 +42,12 @@ pub struct Printer { replace: Option>, /// Whether to prefix each match with the corresponding file name. with_filename: bool, - /// The choice of colors. - color_choice: ColorChoice + /// The color specifications. + colors: ColorSpecs, } -struct ColorChoice { - matched_line: color::Color, - heading: color::Color, - line_number: color::Color -} - -impl ColorChoice { - #[cfg(unix)] - pub fn new() -> ColorChoice { - ColorChoice { - matched_line: color::RED, - heading: color::GREEN, - line_number: color::BLUE - } - } - - #[cfg(not(unix))] - pub fn new() -> ColorChoice { - ColorChoice { - matched_line: color::BRIGHT_RED, - heading: color::BRIGHT_GREEN, - line_number: color::BRIGHT_BLUE - } - } -} - -impl Printer { - /// Create a new printer that writes to wtr. +impl Printer { + /// Create a new printer that writes to wtr with the given color settings. pub fn new(wtr: W) -> Printer { Printer { wtr: wtr, @@ -85,10 +61,16 @@ impl Printer { null: false, replace: None, with_filename: false, - color_choice: ColorChoice::new() + colors: ColorSpecs::default(), } } + /// Set the color specifications. + pub fn colors(mut self, colors: ColorSpecs) -> Printer { + self.colors = colors; + self + } + /// When set, column numbers will be printed for the first match on each /// line. pub fn column(mut self, yes: bool) -> Printer { @@ -285,8 +267,7 @@ impl Printer { let mut last_written = 0; for (s, e) in re.find_iter(buf) { self.write(&buf[last_written..s]); - let _ = self.wtr.fg(self.color_choice.matched_line); - let _ = self.wtr.attr(Attr::Bold); + let _ = self.wtr.set_color(self.colors.matched()); self.write(&buf[s..e]); let _ = self.wtr.reset(); last_written = e; @@ -323,30 +304,20 @@ impl Printer { } fn write_heading>(&mut self, path: P) { - if self.wtr.supports_color() { - let _ = self.wtr.fg(self.color_choice.heading); - let _ = self.wtr.attr(Attr::Bold); - } + let _ = self.wtr.set_color(self.colors.path()); self.write_path(path.as_ref()); + let _ = self.wtr.reset(); if self.null { self.write(b"\x00"); } else { self.write_eol(); } - if self.wtr.supports_color() { - let _ = self.wtr.reset(); - } } fn write_non_heading_path>(&mut self, path: P) { - if self.wtr.supports_color() { - let _ = self.wtr.fg(self.color_choice.heading); - let _ = self.wtr.attr(Attr::Bold); - } + let _ = self.wtr.set_color(self.colors.path()); self.write_path(path.as_ref()); - if self.wtr.supports_color() { - let _ = self.wtr.reset(); - } + let _ = self.wtr.reset(); if self.null { self.write(b"\x00"); } else { @@ -355,14 +326,9 @@ impl Printer { } fn line_number(&mut self, n: u64, sep: u8) { - if self.wtr.supports_color() { - let _ = self.wtr.fg(self.color_choice.line_number); - let _ = self.wtr.attr(Attr::Bold); - } + let _ = self.wtr.set_color(self.colors.line()); self.write(n.to_string().as_bytes()); - if self.wtr.supports_color() { - let _ = self.wtr.reset(); - } + let _ = self.wtr.reset(); self.write(&[sep]); } @@ -397,3 +363,362 @@ impl Printer { } } } + +/// An error that can occur when parsing color specifications. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Error { + /// This occurs when an unrecognized output type is used. + UnrecognizedOutType(String), + /// This occurs when an unrecognized spec type is used. + UnrecognizedSpecType(String), + /// This occurs when an unrecognized color name is used. + UnrecognizedColor(String, String), + /// This occurs when an unrecognized style attribute is used. + UnrecognizedStyle(String), + /// This occurs when the format of a color specification is invalid. + InvalidFormat(String), +} + +impl error::Error for Error { + fn description(&self) -> &str { + match *self { + Error::UnrecognizedOutType(_) => "unrecognized output type", + Error::UnrecognizedSpecType(_) => "unrecognized spec type", + Error::UnrecognizedColor(_, _) => "unrecognized color name", + Error::UnrecognizedStyle(_) => "unrecognized style attribute", + Error::InvalidFormat(_) => "invalid color spec", + } + } + + fn cause(&self) -> Option<&error::Error> { + None + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Error::UnrecognizedOutType(ref name) => { + write!(f, "Unrecognized output type '{}'. Choose from: \ + path, line, match.", name) + } + Error::UnrecognizedSpecType(ref name) => { + write!(f, "Unrecognized spec type '{}'. Choose from: \ + fg, bg, style, none.", name) + } + Error::UnrecognizedColor(_, ref msg) => { + write!(f, "{}", msg) + } + Error::UnrecognizedStyle(ref name) => { + write!(f, "Unrecognized style attribute '{}'. Choose from: \ + nobold, bold.", name) + } + Error::InvalidFormat(ref original) => { + write!(f, "Invalid color speci format: '{}'. Valid format \ + is '(path|line|match):(fg|bg|style):(value)'.", + original) + } + } + } +} + +impl From for Error { + fn from(err: ParseColorError) -> Error { + Error::UnrecognizedColor(err.invalid().to_string(), err.to_string()) + } +} + +/// A merged set of color specifications. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct ColorSpecs { + path: ColorSpec, + line: ColorSpec, + matched: ColorSpec, +} + +/// A single color specification provided by the user. +/// +/// A `ColorSpecs` can be built by merging a sequence of `Spec`s. +/// +/// ## Example +/// +/// The only way to build a `Spec` is to parse it from a string. Once multiple +/// `Spec`s have been constructed, then can be merged into a single +/// `ColorSpecs` value. +/// +/// ```rust +/// use termcolor::{Color, ColorSpecs, Spec}; +/// +/// let spec1: Spec = "path:fg:blue".parse().unwrap(); +/// let spec2: Spec = "match:bg:green".parse().unwrap(); +/// let specs = ColorSpecs::new(&[spec1, spec2]); +/// +/// assert_eq!(specs.path().fg(), Some(Color::Blue)); +/// assert_eq!(specs.matched().bg(), Some(Color::Green)); +/// ``` +/// +/// ## Format +/// +/// The format of a `Spec` is a triple: `{type}:{attribute}:{value}`. Each +/// component is defined as follows: +/// +/// * `{type}` can be one of `path`, `line` or `match`. +/// * `{attribute}` can be one of `fg`, `bg` or `style`. `{attribute}` may also +/// be the special value `none`, in which case, `{value}` can be omitted. +/// * `{value}` is either a color name (for `fg`/`bg`) or a style instruction. +/// +/// `{type}` controls which part of the output should be styled and is +/// application dependent. +/// +/// When `{attribute}` is `none`, then this should cause any existing color +/// settings to be cleared. +/// +/// `{value}` should be a color when `{attribute}` is `fg` or `bg`, or it +/// should be a style instruction when `{attribute}` is `style`. When +/// `{attribute}` is `none`, `{value}` must be omitted. +/// +/// Valid colors are `black`, `blue`, `green`, `red`, `cyan`, `magenta`, +/// `yellow`, `white`. +/// +/// Valid style instructions are `nobold` and `bold`. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Spec { + ty: OutType, + value: SpecValue, +} + +/// The actual value given by the specification. +#[derive(Clone, Debug, Eq, PartialEq)] +enum SpecValue { + None, + Fg(Color), + Bg(Color), + Style(Style), +} + +/// The set of configurable portions of ripgrep's output. +#[derive(Clone, Debug, Eq, PartialEq)] +enum OutType { + Path, + Line, + Match, +} + +/// The specification type. +#[derive(Clone, Debug, Eq, PartialEq)] +enum SpecType { + Fg, + Bg, + Style, + None, +} + +/// The set of available styles for use in the terminal. +#[derive(Clone, Debug, Eq, PartialEq)] +enum Style { + Bold, + NoBold, +} + +impl ColorSpecs { + /// Create color specifications from a list of user supplied + /// specifications. + pub fn new(user_specs: &[Spec]) -> ColorSpecs { + let mut specs = ColorSpecs::default(); + for user_spec in user_specs { + match user_spec.ty { + OutType::Path => user_spec.merge_into(&mut specs.path), + OutType::Line => user_spec.merge_into(&mut specs.line), + OutType::Match => user_spec.merge_into(&mut specs.matched), + } + } + specs + } + + /// Return the color specification for coloring file paths. + fn path(&self) -> &ColorSpec { + &self.path + } + + /// Return the color specification for coloring line numbers. + fn line(&self) -> &ColorSpec { + &self.line + } + + /// Return the color specification for coloring matched text. + fn matched(&self) -> &ColorSpec { + &self.matched + } +} + +impl Spec { + /// Merge this spec into the given color specification. + fn merge_into(&self, cspec: &mut ColorSpec) { + self.value.merge_into(cspec); + } +} + +impl SpecValue { + /// Merge this spec value into the given color specification. + fn merge_into(&self, cspec: &mut ColorSpec) { + match *self { + SpecValue::None => cspec.clear(), + SpecValue::Fg(ref color) => { cspec.set_fg(Some(color.clone())); } + SpecValue::Bg(ref color) => { cspec.set_bg(Some(color.clone())); } + SpecValue::Style(ref style) => { + match *style { + Style::Bold => { cspec.set_bold(true); } + Style::NoBold => { cspec.set_bold(false); } + } + } + } + } +} + +impl FromStr for Spec { + type Err = Error; + + fn from_str(s: &str) -> Result { + let pieces: Vec<&str> = s.split(":").collect(); + if pieces.len() <= 1 || pieces.len() > 3 { + return Err(Error::InvalidFormat(s.to_string())); + } + let otype: OutType = try!(pieces[0].parse()); + match try!(pieces[1].parse()) { + SpecType::None => Ok(Spec { ty: otype, value: SpecValue::None }), + SpecType::Style => { + if pieces.len() < 3 { + return Err(Error::InvalidFormat(s.to_string())); + } + let style: Style = try!(pieces[2].parse()); + Ok(Spec { ty: otype, value: SpecValue::Style(style) }) + } + SpecType::Fg => { + if pieces.len() < 3 { + return Err(Error::InvalidFormat(s.to_string())); + } + let color: Color = try!(pieces[2].parse()); + Ok(Spec { ty: otype, value: SpecValue::Fg(color) }) + } + SpecType::Bg => { + if pieces.len() < 3 { + return Err(Error::InvalidFormat(s.to_string())); + } + let color: Color = try!(pieces[2].parse()); + Ok(Spec { ty: otype, value: SpecValue::Bg(color) }) + } + } + } +} + +impl FromStr for OutType { + type Err = Error; + + fn from_str(s: &str) -> Result { + match &*s.to_lowercase() { + "path" => Ok(OutType::Path), + "line" => Ok(OutType::Line), + "match" => Ok(OutType::Match), + _ => Err(Error::UnrecognizedOutType(s.to_string())), + } + } +} + +impl FromStr for SpecType { + type Err = Error; + + fn from_str(s: &str) -> Result { + match &*s.to_lowercase() { + "fg" => Ok(SpecType::Fg), + "bg" => Ok(SpecType::Bg), + "style" => Ok(SpecType::Style), + "none" => Ok(SpecType::None), + _ => Err(Error::UnrecognizedSpecType(s.to_string())), + } + } +} + +impl FromStr for Style { + type Err = Error; + + fn from_str(s: &str) -> Result { + match &*s.to_lowercase() { + "bold" => Ok(Style::Bold), + "nobold" => Ok(Style::NoBold), + _ => Err(Error::UnrecognizedStyle(s.to_string())), + } + } +} + +#[cfg(test)] +mod tests { + use termcolor::{Color, ColorSpec}; + use super::{ColorSpecs, Error, OutType, Spec, SpecValue, Style}; + + #[test] + fn merge() { + let user_specs: &[Spec] = &[ + "match:fg:blue".parse().unwrap(), + "match:none".parse().unwrap(), + "match:style:bold".parse().unwrap(), + ]; + let mut expect_matched = ColorSpec::new(); + expect_matched.set_bold(true); + assert_eq!(ColorSpecs::new(user_specs), ColorSpecs { + path: ColorSpec::default(), + line: ColorSpec::default(), + matched: expect_matched, + }); + } + + #[test] + fn specs() { + let spec: Spec = "path:fg:blue".parse().unwrap(); + assert_eq!(spec, Spec { + ty: OutType::Path, + value: SpecValue::Fg(Color::Blue), + }); + + let spec: Spec = "path:bg:red".parse().unwrap(); + assert_eq!(spec, Spec { + ty: OutType::Path, + value: SpecValue::Bg(Color::Red), + }); + + let spec: Spec = "match:style:bold".parse().unwrap(); + assert_eq!(spec, Spec { + ty: OutType::Match, + value: SpecValue::Style(Style::Bold), + }); + + let spec: Spec = "line:none".parse().unwrap(); + assert_eq!(spec, Spec { + ty: OutType::Line, + value: SpecValue::None, + }); + } + + #[test] + fn spec_errors() { + let err = "line:nonee".parse::().unwrap_err(); + assert_eq!(err, Error::UnrecognizedSpecType("nonee".to_string())); + + let err = "".parse::().unwrap_err(); + assert_eq!(err, Error::InvalidFormat("".to_string())); + + let err = "foo".parse::().unwrap_err(); + assert_eq!(err, Error::InvalidFormat("foo".to_string())); + + let err = "line:style:italic".parse::().unwrap_err(); + assert_eq!(err, Error::UnrecognizedStyle("italic".to_string())); + + let err = "line:fg:brown".parse::().unwrap_err(); + match err { + Error::UnrecognizedColor(name, _) => assert_eq!(name, "brown"), + err => assert!(false, "unexpected error: {:?}", err), + } + + let err = "foo:fg:brown".parse::().unwrap_err(); + assert_eq!(err, Error::UnrecognizedOutType("foo".to_string())); + } +} diff --git a/src/search_buffer.rs b/src/search_buffer.rs index 16a161e1b..1e8cfe8f3 100644 --- a/src/search_buffer.rs +++ b/src/search_buffer.rs @@ -10,7 +10,7 @@ use std::cmp; use std::path::Path; use grep::Grep; -use term::Terminal; +use termcolor::WriteColor; use printer::Printer; use search_stream::{IterLines, Options, count_lines, is_binary}; @@ -26,7 +26,7 @@ pub struct BufferSearcher<'a, W: 'a> { last_line: usize, } -impl<'a, W: Send + Terminal> BufferSearcher<'a, W> { +impl<'a, W: WriteColor> BufferSearcher<'a, W> { pub fn new( printer: &'a mut Printer, grep: &'a Grep, @@ -196,10 +196,9 @@ mod tests { use std::path::Path; use grep::GrepBuilder; - use term::{Terminal, TerminfoTerminal}; - use out::ColoredTerminal; use printer::Printer; + use termcolor; use super::BufferSearcher; @@ -216,15 +215,14 @@ and exhibited clearly, with a label attached.\ &Path::new("/baz.rs") } - type TestSearcher<'a> = - BufferSearcher<'a, ColoredTerminal>>>; + type TestSearcher<'a> = BufferSearcher<'a, termcolor::NoColor>>; fn search TestSearcher>( pat: &str, haystack: &str, mut map: F, ) -> (u64, String) { - let outbuf = ColoredTerminal::NoColor(vec![]); + let outbuf = termcolor::NoColor::new(vec![]); let mut pp = Printer::new(outbuf).with_filename(true); let grep = GrepBuilder::new(pat).build().unwrap(); let count = { diff --git a/src/search_stream.rs b/src/search_stream.rs index d4478170d..d5447451f 100644 --- a/src/search_stream.rs +++ b/src/search_stream.rs @@ -13,7 +13,7 @@ use std::path::{Path, PathBuf}; use bytecount; use grep::{Grep, Match}; use memchr::{memchr, memrchr}; -use term::Terminal; +use termcolor::WriteColor; use printer::Printer; @@ -136,7 +136,7 @@ impl Options { } } -impl<'a, R: io::Read, W: Terminal + Send> Searcher<'a, R, W> { +impl<'a, R: io::Read, W: WriteColor> Searcher<'a, R, W> { /// Create a new searcher. /// /// `inp` is a reusable input buffer that is used as scratch space by this @@ -763,10 +763,8 @@ mod tests { use std::path::Path; use grep::GrepBuilder; - use term::{Terminal, TerminfoTerminal}; - - use out::ColoredTerminal; use printer::Printer; + use termcolor; use super::{InputBuffer, Searcher, start_of_previous_lines}; @@ -806,7 +804,7 @@ fn main() { type TestSearcher<'a> = Searcher< 'a, io::Cursor>, - ColoredTerminal>>, + termcolor::NoColor>, >; fn search_smallcap TestSearcher>( @@ -815,7 +813,7 @@ fn main() { mut map: F, ) -> (u64, String) { let mut inp = InputBuffer::with_capacity(1); - let outbuf = ColoredTerminal::NoColor(vec![]); + let outbuf = termcolor::NoColor::new(vec![]); let mut pp = Printer::new(outbuf).with_filename(true); let grep = GrepBuilder::new(pat).build().unwrap(); let count = { @@ -832,7 +830,7 @@ fn main() { mut map: F, ) -> (u64, String) { let mut inp = InputBuffer::with_capacity(4096); - let outbuf = ColoredTerminal::NoColor(vec![]); + let outbuf = termcolor::NoColor::new(vec![]); let mut pp = Printer::new(outbuf).with_filename(true); let grep = GrepBuilder::new(pat).build().unwrap(); let count = { diff --git a/src/terminal_win.rs b/src/terminal_win.rs deleted file mode 100644 index 89f1e7342..000000000 --- a/src/terminal_win.rs +++ /dev/null @@ -1,176 +0,0 @@ -/*! -This module contains a Windows-only *in-memory* implementation of the -`term::Terminal` trait. - -This particular implementation is a bit idiosyncratic, and the "in-memory" -specification is to blame. In particular, on Windows, coloring requires -communicating with the console synchronously as data is written to stdout. -This is anathema to how ripgrep fundamentally works: by writing search results -to intermediate thread local buffers in order to maximize parallelism. - -Eliminating parallelism on Windows isn't an option, because that would negate -a tremendous performance benefit just for coloring. - -We've worked around this by providing an implementation of `term::Terminal` -that records precisely where a color or a reset should be invoked, according -to a byte offset in the in memory buffer. When the buffer is actually printed, -we copy the bytes from the buffer to stdout incrementally while invoking the -corresponding console APIs for coloring at the right location. - -(Another approach would be to do ANSI coloring unconditionally, then parse that -and translate it to console commands. The advantage of that approach is that -it doesn't require any additional memory for storing offsets. In practice -though, coloring is only used in the terminal, which tends to correspond to -searches that produce very few results with respect to the corpus searched. -Therefore, this is an acceptable trade off. Namely, we do not pay for it when -coloring is disabled. -*/ -use std::io; - -use term::{self, Terminal}; -use term::color::Color; - -/// An in-memory buffer that provides Windows console coloring. -#[derive(Clone, Debug)] -pub struct WindowsBuffer { - buf: Vec, - pos: usize, - colors: Vec, -} - -/// A color associated with a particular location in a buffer. -#[derive(Clone, Debug)] -struct WindowsColor { - pos: usize, - opt: WindowsOption, -} - -/// A color or reset directive that can be translated into an instruction to -/// the Windows console. -#[derive(Clone, Debug)] -enum WindowsOption { - Foreground(Color), - Background(Color), - Reset, -} - -impl WindowsBuffer { - /// Create a new empty buffer for Windows console coloring. - pub fn new() -> WindowsBuffer { - WindowsBuffer { - buf: vec![], - pos: 0, - colors: vec![], - } - } - - fn push(&mut self, opt: WindowsOption) { - let pos = self.pos; - self.colors.push(WindowsColor { pos: pos, opt: opt }); - } - - /// Print the contents to the given terminal. - pub fn print_stdout(&self, tt: &mut T) { - if !tt.supports_color() { - let _ = tt.write_all(&self.buf); - let _ = tt.flush(); - return; - } - let mut last = 0; - for col in &self.colors { - let _ = tt.write_all(&self.buf[last..col.pos]); - match col.opt { - WindowsOption::Foreground(c) => { - let _ = tt.fg(c); - } - WindowsOption::Background(c) => { - let _ = tt.bg(c); - } - WindowsOption::Reset => { - let _ = tt.reset(); - } - } - last = col.pos; - } - let _ = tt.write_all(&self.buf[last..]); - let _ = tt.flush(); - } - - /// Clear the buffer. - pub fn clear(&mut self) { - self.buf.clear(); - self.colors.clear(); - self.pos = 0; - } -} - -impl io::Write for WindowsBuffer { - fn write(&mut self, buf: &[u8]) -> io::Result { - let n = try!(self.buf.write(buf)); - self.pos += n; - Ok(n) - } - - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -impl Terminal for WindowsBuffer { - type Output = Vec; - - fn fg(&mut self, fg: Color) -> term::Result<()> { - self.push(WindowsOption::Foreground(fg)); - Ok(()) - } - - fn bg(&mut self, bg: Color) -> term::Result<()> { - self.push(WindowsOption::Background(bg)); - Ok(()) - } - - fn attr(&mut self, _attr: term::Attr) -> term::Result<()> { - Err(term::Error::NotSupported) - } - - fn supports_attr(&self, _attr: term::Attr) -> bool { - false - } - - fn reset(&mut self) -> term::Result<()> { - self.push(WindowsOption::Reset); - Ok(()) - } - - fn supports_reset(&self) -> bool { - true - } - - fn supports_color(&self) -> bool { - true - } - - fn cursor_up(&mut self) -> term::Result<()> { - Err(term::Error::NotSupported) - } - - fn delete_line(&mut self) -> term::Result<()> { - Err(term::Error::NotSupported) - } - - fn carriage_return(&mut self) -> term::Result<()> { - Err(term::Error::NotSupported) - } - - fn get_ref(&self) -> &Vec { - &self.buf - } - - fn get_mut(&mut self) -> &mut Vec { - &mut self.buf - } - - fn into_inner(self) -> Vec { - self.buf - } -} diff --git a/src/worker.rs b/src/worker.rs index 23ed75496..0e700dfb6 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -5,7 +5,7 @@ use std::path::Path; use grep::Grep; use ignore::DirEntry; use memmap::{Mmap, Protection}; -use term::Terminal; +use termcolor::WriteColor; use pathutil::strip_prefix; use printer::Printer; @@ -182,7 +182,7 @@ impl Worker { /// Execute the worker with the given printer and work item. /// /// A work item can either be stdin or a file path. - pub fn run( + pub fn run( &mut self, printer: &mut Printer, work: Work, @@ -227,7 +227,7 @@ impl Worker { } } - fn search( + fn search( &mut self, printer: &mut Printer, path: &Path, @@ -251,7 +251,7 @@ impl Worker { .map_err(From::from) } - fn search_mmap( + fn search_mmap( &mut self, printer: &mut Printer, path: &Path, diff --git a/termcolor/Cargo.toml b/termcolor/Cargo.toml new file mode 100644 index 000000000..2814a6acb --- /dev/null +++ b/termcolor/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "termcolor" +version = "0.1.0" #:version +authors = ["Andrew Gallant "] +description = """ +A simple cross platform library for writing colored text to a terminal. +""" +documentation = "https://docs.rs/termcolor" +homepage = "https://github.com/BurntSushi/ripgrep/tree/master/termcolor" +repository = "https://github.com/BurntSushi/ripgrep/tree/master/termcolor" +readme = "README.md" +keywords = ["windows", "win", "color", "ansi", "console"] +license = "Unlicense/MIT" + +[lib] +name = "termcolor" +bench = false + +[target.'cfg(windows)'.dependencies] +wincolor = { version = "0.1.0", path = "../wincolor" } diff --git a/termcolor/README.md b/termcolor/README.md new file mode 100644 index 000000000..986015904 --- /dev/null +++ b/termcolor/README.md @@ -0,0 +1,88 @@ +termcolor +========= +A simple cross platform library for writing colored text to a terminal. This +library writes colored text either using standard ANSI escape sequences or +by interacting with the Windows console. Several convenient abstractions +are provided for use in single-threaded or multi-threaded command line +applications. + +[![Windows build status](https://ci.appveyor.com/api/projects/status/github/BurntSushi/ripgrep?svg=true)](https://ci.appveyor.com/project/BurntSushi/ripgrep) +[![](https://img.shields.io/crates/v/wincolor.svg)](https://crates.io/crates/wincolor) + +[![Linux build status](https://api.travis-ci.org/BurntSushi/ripgrep.png)](https://travis-ci.org/BurntSushi/ripgrep) +[![Windows build status](https://ci.appveyor.com/api/projects/status/github/BurntSushi/ripgrep?svg=true)](https://ci.appveyor.com/project/BurntSushi/ripgrep) +[![](https://img.shields.io/crates/v/termcolor.svg)](https://crates.io/crates/termcolor) + +Dual-licensed under MIT or the [UNLICENSE](http://unlicense.org). + +### Documentation + +[https://docs.rs/termcolor](https://docs.rs/termcolor) + +### Usage + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +termcolor = "0.1" +``` + +and this to your crate root: + +```rust +extern crate termcolor; +``` + +### Organization + +The `WriteColor` trait extends the `io::Write` trait with methods for setting +colors or resetting them. + +`Stdout` and `StdoutLock` both satisfy `WriteColor` and are analogous to +`std::io::Stdout` and `std::io::StdoutLock`. + +`Buffer` is an in memory buffer that supports colored text. In a parallel +program, each thread might write to its own buffer. A buffer can be printed +to stdout using a `BufferWriter`. The advantage of this design is that +each thread can work in parallel on a buffer without having to synchronize +access to global resources such as the Windows console. Moreover, this design +also prevents interleaving of buffer output. + +`Ansi` and `NoColor` both satisfy `WriteColor` for arbitrary implementors of +`io::Write`. These types are useful when you know exactly what you need. An +analogous type for the Windows console is not provided since it cannot exist. + +### Example: using `Stdout` + +The `Stdout` type in this crate works similarly to `std::io::Stdout`, except +it is augmented with methods for coloring by the `WriteColor` trait. For +example, to write some green text: + +```rust +use std::io::Write; +use termcolor::{Color, ColorChoice, ColorSpec, Stdout, WriteColor}; + +let mut stdout = Stdout::new(ColorChoice::Always); +try!(stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))); +try!(writeln!(&mut stdout, "green text!")); +``` + +### Example: using `BufferWriter` + +A `BufferWriter` can create buffers and write buffers to stdout. It does *not* +implement `io::Write` or `WriteColor` itself. Instead, `Buffer` implements +`io::Write` and `io::WriteColor`. + +This example shows how to print some green text to stdout. + +```rust +use std::io::Write; +use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor}; + +let mut bufwtr = BufferWriter::stdout(ColorChoice::Always); +let mut buffer = bufwtr.buffer(); +try!(buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)))); +try!(writeln!(&mut buffer, "green text!")); +try!(bufwtr.print(&buffer)); +``` diff --git a/termcolor/src/lib.rs b/termcolor/src/lib.rs new file mode 100644 index 000000000..efaffbd2e --- /dev/null +++ b/termcolor/src/lib.rs @@ -0,0 +1,1071 @@ +/*! +This crate provides a cross platform abstraction for writing colored text to +a terminal. Colors are written using either ANSI escape sequences or by +communicating with a Windows console. Much of this API was motivated by use +inside command line applications, where colors or styles can be configured +by the end user and/or the environment. + +This crate also provides platform independent support for writing colored text +to an in memory buffer. While this is easy to do with ANSI escape sequences +(because they are in the buffer themselves), it is trickier to do with the +Windows console API, which requires synchronous communication. + +# Organization + +The `WriteColor` trait extends the `io::Write` trait with methods for setting +colors or resetting them. + +`Stdout` and `StdoutLock` both satisfy `WriteColor` and are analogous to +`std::io::Stdout` and `std::io::StdoutLock`. + +`Buffer` is an in memory buffer that supports colored text. In a parallel +program, each thread might write to its own buffer. A buffer can be printed +to stdout using a `BufferWriter`. The advantage of this design is that +each thread can work in parallel on a buffer without having to synchronize +access to global resources such as the Windows console. Moreover, this design +also prevents interleaving of buffer output. + +`Ansi` and `NoColor` both satisfy `WriteColor` for arbitrary implementors of +`io::Write`. These types are useful when you know exactly what you need. An +analogous type for the Windows console is not provided since it cannot exist. + +# Example: using `Stdout` + +The `Stdout` type in this crate works similarly to `std::io::Stdout`, except +it is augmented with methods for coloring by the `WriteColor` trait. For +example, to write some green text: + +```rust,no_run +# fn test() -> Result<(), Box<::std::error::Error>> { +use std::io::Write; +use termcolor::{Color, ColorChoice, ColorSpec, Stdout, WriteColor}; + +let mut stdout = Stdout::new(ColorChoice::Always); +try!(stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))); +try!(writeln!(&mut stdout, "green text!")); +# Ok(()) } +``` + +# Example: using `BufferWriter` + +A `BufferWriter` can create buffers and write buffers to stdout. It does *not* +implement `io::Write` or `WriteColor` itself. Instead, `Buffer` implements +`io::Write` and `io::WriteColor`. + +This example shows how to print some green text to stdout. + +```rust,no_run +# fn test() -> Result<(), Box<::std::error::Error>> { +use std::io::Write; +use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor}; + +let mut bufwtr = BufferWriter::stdout(ColorChoice::Always); +let mut buffer = bufwtr.buffer(); +try!(buffer.set_color(ColorSpec::new().set_fg(Some(Color::Green)))); +try!(writeln!(&mut buffer, "green text!")); +try!(bufwtr.print(&buffer)); +# Ok(()) } +``` +*/ +#![deny(missing_docs)] + +#[cfg(windows)] +extern crate wincolor; + +use std::env; +use std::error; +use std::fmt; +use std::io::{self, Write}; +use std::str::FromStr; +#[cfg(windows)] +use std::sync::{Mutex, MutexGuard}; +use std::sync::atomic::{AtomicBool, Ordering}; + +/// This trait describes the behavior of writers that support colored output. +pub trait WriteColor: io::Write { + /// Returns true if and only if the underlying writer supports colors. + fn supports_color(&self) -> bool; + + /// Set the color settings of the writer. + /// + /// Subsequent writes to this writer will use these settings until either + /// `reset` is called or new color settings are set. + /// + /// If there was a problem setting the color settings, then an error is + /// returned. + fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()>; + + /// Reset the current color settings to their original settings. + /// + /// If there was a problem resetting the color settings, then an error is + /// returned. + fn reset(&mut self) -> io::Result<()>; +} + +impl<'a, T: WriteColor> WriteColor for &'a mut T { + fn supports_color(&self) -> bool { (&**self).supports_color() } + fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { + (&mut **self).set_color(spec) + } + fn reset(&mut self) -> io::Result<()> { (&mut **self).reset() } +} + +/// ColorChoice represents the color preferences of an end user. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ColorChoice { + /// Try very hard to emit colors. This includes emitting ANSI colors + /// on Windows if the console API is unavailable. + Always, + /// AlwaysAnsi is like Always, except it never tries to use anything other + /// than emitting ANSI color codes. + AlwaysAnsi, + /// Try to use colors, but don't force the issue. If the console isn't + /// available on Windows, or if TERM=dumb, for example, then don't use + /// colors. + Auto, + /// Never emit colors. + Never, +} + +impl ColorChoice { + /// Returns true if we should attempt to write colored output. + #[cfg(not(windows))] + fn should_attempt_color(&self) -> bool { + match *self { + ColorChoice::Always => true, + ColorChoice::AlwaysAnsi => true, + ColorChoice::Never => false, + ColorChoice::Auto => { + match env::var("TERM") { + Err(_) => false, + Ok(k) => k != "dumb", + } + } + } + } + + /// Returns true if we should attempt to write colored output. + #[cfg(windows)] + fn should_attempt_color(&self) -> bool { + match *self { + ColorChoice::Always => true, + ColorChoice::AlwaysAnsi => true, + ColorChoice::Never => false, + ColorChoice::Auto => { + match env::var("TERM") { + Err(_) => true, + Ok(k) => k != "dumb", + } + } + } + } + + /// Returns true if this choice should forcefully use ANSI color codes. + /// + /// It's possible that ANSI is still the correct choice even if this + /// returns false. + #[cfg(windows)] + fn should_ansi(&self) -> bool { + match *self { + ColorChoice::Always => false, + ColorChoice::AlwaysAnsi => true, + ColorChoice::Never => false, + ColorChoice::Auto => { + match env::var("TERM") { + Err(_) => false, + // cygwin doesn't seem to support ANSI escape sequences + // and instead has its own variety. However, the Windows + // console API may be available. + Ok(k) => k != "dumb" && k != "cygwin", + } + } + } + } +} + +/// Satisfies `io::Write` and `WriteColor`, and supports optional coloring +/// to stdout. +pub struct Stdout { + wtr: WriterInner<'static, io::Stdout>, +} + +/// `StdoutLock` is a locked reference to a `Stdout`. +/// +/// This implements the `io::Write` and `WriteColor` traits, and is constructed +/// via the `Write::lock` method. +/// +/// The lifetime `'a` refers to the lifetime of the corresponding `Stdout`. +pub struct StdoutLock<'a> { + wtr: WriterInner<'a, io::StdoutLock<'a>>, +} + +/// WriterInner is a (limited) generic representation of a writer. It is +/// limited because W should only ever be stdout/stderr on Windows. +enum WriterInner<'a, W> { + NoColor(NoColor), + Ansi(Ansi), + /// What a gross hack. On Windows, we need to specify a lifetime for the + /// console when in a locked state, but obviously don't need to do that + /// on Unix, which make the `'a` unused. To satisfy the compiler, we need + /// a PhantomData. + #[allow(dead_code)] + Unreachable(::std::marker::PhantomData<&'a ()>), + #[cfg(windows)] + Windows { wtr: W, console: Mutex }, + #[cfg(windows)] + WindowsLocked { wtr: W, console: MutexGuard<'a, wincolor::Console> }, +} + +impl Stdout { + /// Create a new `Stdout` with the given color preferences. + /// + /// The specific color/style settings can be configured when writing via + /// the `WriteColor` trait. + #[cfg(not(windows))] + pub fn new(choice: ColorChoice) -> Stdout { + let wtr = + if choice.should_attempt_color() { + WriterInner::Ansi(Ansi(io::stdout())) + } else { + WriterInner::NoColor(NoColor(io::stdout())) + }; + Stdout { wtr: wtr } + } + + /// Create a new `Stdout` with the given color preferences. + /// + /// If coloring is desired and a Windows console could not be found, then + /// ANSI escape sequences are used instead. + /// + /// The specific color/style settings can be configured when writing via + /// the `WriteColor` trait. + #[cfg(windows)] + pub fn new(choice: ColorChoice) -> Stdout { + let wtr = + if choice.should_attempt_color() { + if choice.should_ansi() { + WriterInner::Ansi(Ansi(io::stdout())) + } else if let Ok(console) = wincolor::Console::stdout() { + WriterInner::Windows { + wtr: io::stdout(), + console: Mutex::new(console), + } + } else { + WriterInner::Ansi(Ansi(io::stdout())) + } + } else { + WriterInner::NoColor(NoColor(io::stdout())) + }; + Stdout { wtr: wtr } + } + + /// Lock the underlying writer. + /// + /// The lock guard returned also satisfies `io::Write` and + /// `WriteColor`. + /// + /// This method is **not reentrant**. It may panic if `lock` is called + /// while a `StdoutLock` is still alive. + pub fn lock(&self) -> StdoutLock { + let locked = match self.wtr { + WriterInner::Unreachable(_) => unreachable!(), + WriterInner::NoColor(ref w) => { + WriterInner::NoColor(NoColor(w.0.lock())) + } + WriterInner::Ansi(ref w) => { + WriterInner::Ansi(Ansi(w.0.lock())) + } + #[cfg(windows)] + WriterInner::Windows { ref wtr, ref console } => { + WriterInner::WindowsLocked { + wtr: wtr.lock(), + console: console.lock().unwrap(), + } + } + #[cfg(windows)] + WriterInner::WindowsLocked{..} => { + panic!("cannot call Stdout.lock while a StdoutLock is alive"); + } + }; + StdoutLock { wtr: locked } + } +} + +impl io::Write for Stdout { + fn write(&mut self, b: &[u8]) -> io::Result { self.wtr.write(b) } + fn flush(&mut self) -> io::Result<()> { self.wtr.flush() } +} + +impl WriteColor for Stdout { + fn supports_color(&self) -> bool { self.wtr.supports_color() } + fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { + self.wtr.set_color(spec) + } + fn reset(&mut self) -> io::Result<()> { self.wtr.reset() } +} + +impl<'a> io::Write for StdoutLock<'a> { + fn write(&mut self, b: &[u8]) -> io::Result { self.wtr.write(b) } + fn flush(&mut self) -> io::Result<()> { self.wtr.flush() } +} + +impl<'a> WriteColor for StdoutLock<'a> { + fn supports_color(&self) -> bool { self.wtr.supports_color() } + fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { + self.wtr.set_color(spec) + } + fn reset(&mut self) -> io::Result<()> { self.wtr.reset() } +} + +impl<'a, W: io::Write> io::Write for WriterInner<'a, W> { + fn write(&mut self, buf: &[u8]) -> io::Result { + match *self { + WriterInner::Unreachable(_) => unreachable!(), + WriterInner::NoColor(ref mut wtr) => wtr.write(buf), + WriterInner::Ansi(ref mut wtr) => wtr.write(buf), + #[cfg(windows)] + WriterInner::Windows { ref mut wtr, .. } => wtr.write(buf), + #[cfg(windows)] + WriterInner::WindowsLocked { ref mut wtr, .. } => wtr.write(buf), + } + } + + fn flush(&mut self) -> io::Result<()> { + match *self { + WriterInner::Unreachable(_) => unreachable!(), + WriterInner::NoColor(ref mut wtr) => wtr.flush(), + WriterInner::Ansi(ref mut wtr) => wtr.flush(), + #[cfg(windows)] + WriterInner::Windows { ref mut wtr, .. } => wtr.flush(), + #[cfg(windows)] + WriterInner::WindowsLocked { ref mut wtr, .. } => wtr.flush(), + } + } +} + +impl<'a, W: io::Write> WriteColor for WriterInner<'a, W> { + fn supports_color(&self) -> bool { + match *self { + WriterInner::Unreachable(_) => unreachable!(), + WriterInner::NoColor(_) => false, + WriterInner::Ansi(_) => true, + #[cfg(windows)] + WriterInner::Windows { .. } => true, + #[cfg(windows)] + WriterInner::WindowsLocked { .. } => true, + } + } + + fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { + match *self { + WriterInner::Unreachable(_) => unreachable!(), + WriterInner::NoColor(ref mut wtr) => wtr.set_color(spec), + WriterInner::Ansi(ref mut wtr) => wtr.set_color(spec), + #[cfg(windows)] + WriterInner::Windows { ref mut wtr, ref console } => { + try!(wtr.flush()); + let mut console = console.lock().unwrap(); + spec.write_console(&mut *console) + } + #[cfg(windows)] + WriterInner::WindowsLocked { ref mut wtr, ref mut console } => { + try!(wtr.flush()); + spec.write_console(console) + } + } + } + + fn reset(&mut self) -> io::Result<()> { + match *self { + WriterInner::Unreachable(_) => unreachable!(), + WriterInner::NoColor(ref mut wtr) => wtr.reset(), + WriterInner::Ansi(ref mut wtr) => wtr.reset(), + #[cfg(windows)] + WriterInner::Windows { ref mut wtr, ref mut console } => { + try!(wtr.flush()); + try!(console.lock().unwrap().reset()); + Ok(()) + } + #[cfg(windows)] + WriterInner::WindowsLocked { ref mut wtr, ref mut console } => { + try!(wtr.flush()); + try!(console.reset()); + Ok(()) + } + } + } +} + +/// Writes colored buffers to stdout. +/// +/// Writable buffers can be obtained by calling `buffer` on a `BufferWriter`. +/// +/// This writer works with terminals that support ANSI escape sequences or +/// with a Windows console. +/// +/// It is intended for a `BufferWriter` to be put in an `Arc` and written to +/// from multiple threads simultaneously. +pub struct BufferWriter { + stdout: io::Stdout, + printed: AtomicBool, + separator: Option>, + color_choice: ColorChoice, + #[cfg(windows)] + console: Option>, +} + +impl BufferWriter { + /// Create a new `BufferWriter` that writes to stdout with the given + /// color preferences. + /// + /// The specific color/style settings can be configured when writing to + /// the buffers themselves. + #[cfg(not(windows))] + pub fn stdout(choice: ColorChoice) -> BufferWriter { + BufferWriter { + stdout: io::stdout(), + printed: AtomicBool::new(false), + separator: None, + color_choice: choice, + } + } + + /// Create a new `BufferWriter` that writes to stdout with the given + /// color preferences. + /// + /// If coloring is desired and a Windows console could not be found, then + /// ANSI escape sequences are used instead. + /// + /// The specific color/style settings can be configured when writing to + /// the buffers themselves. + #[cfg(windows)] + pub fn stdout(choice: ColorChoice) -> BufferWriter { + BufferWriter { + stdout: io::stdout(), + printed: AtomicBool::new(false), + separator: None, + color_choice: choice, + console: wincolor::Console::stdout().ok().map(Mutex::new), + } + } + + /// If set, the separator given is printed between buffers. By default, no + /// separator is printed. + /// + /// The default value is `None`. + pub fn separator(&mut self, sep: Option>) { + self.separator = sep; + } + + /// Creates a new `Buffer` with the current color preferences. + /// + /// A `Buffer` satisfies both `io::Write` and `WriteColor`. A `Buffer` can + /// be printed using the `print` method. + #[cfg(not(windows))] + pub fn buffer(&self) -> Buffer { + Buffer::new(self.color_choice) + } + + /// Creates a new `Buffer` with the current color preferences. + /// + /// A `Buffer` satisfies both `io::Write` and `WriteColor`. A `Buffer` can + /// be printed using the `print` method. + #[cfg(windows)] + pub fn buffer(&self) -> Buffer { + Buffer::new(self.color_choice, self.console.is_some()) + } + + /// Prints the contents of the given buffer. + /// + /// It is safe to call this from multiple threads simultaneously. In + /// particular, all buffers are written atomically. No interleaving will + /// occur. + pub fn print(&self, buf: &Buffer) -> io::Result<()> { + if buf.is_empty() { + return Ok(()); + } + let mut stdout = self.stdout.lock(); + if let Some(ref sep) = self.separator { + if self.printed.load(Ordering::SeqCst) { + try!(stdout.write_all(sep)); + try!(stdout.write_all(b"\n")); + } + } + match buf.0 { + BufferInner::NoColor(ref b) => try!(stdout.write_all(&b.0)), + BufferInner::Ansi(ref b) => try!(stdout.write_all(&b.0)), + #[cfg(windows)] + BufferInner::Windows(ref b) => { + // We guarantee by construction that we have a console here. + // Namely, a BufferWriter is the only way to produce a Buffer. + let console_mutex = self.console.as_ref() + .expect("got Windows buffer but have no Console"); + let mut console = console_mutex.lock().unwrap(); + try!(b.print(&mut *console, &mut stdout)); + } + } + self.printed.store(true, Ordering::SeqCst); + Ok(()) + } +} + +/// Write colored text to memory. +/// +/// `Buffer` is a platform independent abstraction for printing colored text to +/// an in memory buffer. When the buffer is printed using a `BufferWriter`, the +/// color information will be applied to the output device (a tty on Unix and a +/// console on Windows). +/// +/// A `Buffer` is typically created by calling the `BufferWriter.buffer` +/// method, which will take color preferences and the environment into +/// account. However, buffers can also be manually created using `no_color`, +/// `ansi` or `console` (on Windows). +pub struct Buffer(BufferInner); + +/// BufferInner is an enumeration of different buffer types. +enum BufferInner { + /// No coloring information should be applied. This ignores all coloring + /// directives. + NoColor(NoColor>), + /// Apply coloring using ANSI escape sequences embedded into the buffer. + Ansi(Ansi>), + /// Apply coloring using the Windows console APIs. This buffer saves + /// color information in memory and only interacts with the console when + /// the buffer is printed. + #[cfg(windows)] + Windows(WindowsBuffer), +} + +impl Buffer { + /// Create a new buffer with the given color settings. + #[cfg(not(windows))] + fn new(choice: ColorChoice) -> Buffer { + if choice.should_attempt_color() { + Buffer::ansi() + } else { + Buffer::no_color() + } + } + + /// Create a new buffer with the given color settings. + /// + /// On Windows, one can elect to create a buffer capable of being written + /// to a console. Only enable it if a console is available. + /// + /// If coloring is desired and `console` is false, then ANSI escape + /// sequences are used instead. + #[cfg(windows)] + fn new(choice: ColorChoice, console: bool) -> Buffer { + if choice.should_attempt_color() { + if !console || choice.should_ansi() { + Buffer::ansi() + } else { + Buffer::console() + } + } else { + Buffer::no_color() + } + } + + /// Create a buffer that drops all color information. + pub fn no_color() -> Buffer { + Buffer(BufferInner::NoColor(NoColor(vec![]))) + } + + /// Create a buffer that uses ANSI escape sequences. + pub fn ansi() -> Buffer { + Buffer(BufferInner::Ansi(Ansi(vec![]))) + } + + /// Create a buffer that can be written to a Windows console. + #[cfg(windows)] + pub fn console() -> Buffer { + Buffer(BufferInner::Windows(WindowsBuffer::new())) + } + + /// Returns true if and only if this buffer is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the length of this buffer in bytes. + pub fn len(&self) -> usize { + match self.0 { + BufferInner::NoColor(ref b) => b.0.len(), + BufferInner::Ansi(ref b) => b.0.len(), + #[cfg(windows)] + BufferInner::Windows(ref b) => b.buf.len(), + } + } + + /// Clears this buffer. + pub fn clear(&mut self) { + match self.0 { + BufferInner::NoColor(ref mut b) => b.0.clear(), + BufferInner::Ansi(ref mut b) => b.0.clear(), + #[cfg(windows)] + BufferInner::Windows(ref mut b) => b.clear(), + } + } + + /// Consume this buffer and return the underlying raw data. + /// + /// On Windows, this unrecoverably drops all color information associated + /// with the buffer. + pub fn into_inner(self) -> Vec { + match self.0 { + BufferInner::NoColor(b) => b.0, + BufferInner::Ansi(b) => b.0, + #[cfg(windows)] + BufferInner::Windows(b) => b.buf, + } + } + + /// Return the underlying data of the buffer. + pub fn as_slice(&self) -> &[u8] { + match self.0 { + BufferInner::NoColor(ref b) => &b.0, + BufferInner::Ansi(ref b) => &b.0, + #[cfg(windows)] + BufferInner::Windows(ref b) => &b.buf, + } + } + + /// Return the underlying data of the buffer as a mutable slice. + pub fn as_mut_slice(&mut self) -> &mut [u8] { + match self.0 { + BufferInner::NoColor(ref mut b) => &mut b.0, + BufferInner::Ansi(ref mut b) => &mut b.0, + #[cfg(windows)] + BufferInner::Windows(ref mut b) => &mut b.buf, + } + } +} + +impl io::Write for Buffer { + fn write(&mut self, buf: &[u8]) -> io::Result { + match self.0 { + BufferInner::NoColor(ref mut w) => w.write(buf), + BufferInner::Ansi(ref mut w) => w.write(buf), + #[cfg(windows)] + BufferInner::Windows(ref mut w) => w.write(buf), + } + } + + fn flush(&mut self) -> io::Result<()> { + match self.0 { + BufferInner::NoColor(ref mut w) => w.flush(), + BufferInner::Ansi(ref mut w) => w.flush(), + #[cfg(windows)] + BufferInner::Windows(ref mut w) => w.flush(), + } + } +} + +impl WriteColor for Buffer { + fn supports_color(&self) -> bool { + match self.0 { + BufferInner::NoColor(_) => false, + BufferInner::Ansi(_) => true, + #[cfg(windows)] + BufferInner::Windows(_) => true, + } + } + + fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { + match self.0 { + BufferInner::NoColor(ref mut w) => w.set_color(spec), + BufferInner::Ansi(ref mut w) => w.set_color(spec), + #[cfg(windows)] + BufferInner::Windows(ref mut w) => w.set_color(spec), + } + } + + fn reset(&mut self) -> io::Result<()> { + match self.0 { + BufferInner::NoColor(ref mut w) => w.reset(), + BufferInner::Ansi(ref mut w) => w.reset(), + #[cfg(windows)] + BufferInner::Windows(ref mut w) => w.reset(), + } + } +} + +/// Satisfies `WriteColor` but ignores all color options. +pub struct NoColor(W); + +impl NoColor { + /// Create a new writer that satisfies `WriteColor` but drops all color + /// information. + pub fn new(wtr: W) -> NoColor { NoColor(wtr) } + + /// Consume this `NoColor` value and return the inner writer. + pub fn into_inner(self) -> W { self.0 } + + /// Return a reference to the inner writer. + pub fn get_ref(&self) -> &W { &self.0 } + + /// Return a mutable reference to the inner writer. + pub fn get_mut(&mut self) -> &mut W { &mut self.0 } +} + +impl io::Write for NoColor { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.0.flush() + } +} + +impl WriteColor for NoColor { + fn supports_color(&self) -> bool { false } + fn set_color(&mut self, _: &ColorSpec) -> io::Result<()> { Ok(()) } + fn reset(&mut self) -> io::Result<()> { Ok(()) } +} + +/// Satisfies `WriteColor` using standard ANSI escape sequences. +pub struct Ansi(W); + +impl Ansi { + /// Create a new writer that satisfies `WriteColor` using standard ANSI + /// escape sequences. + pub fn new(wtr: W) -> Ansi { Ansi(wtr) } + + /// Consume this `Ansi` value and return the inner writer. + pub fn into_inner(self) -> W { self.0 } + + /// Return a reference to the inner writer. + pub fn get_ref(&self) -> &W { &self.0 } + + /// Return a mutable reference to the inner writer. + pub fn get_mut(&mut self) -> &mut W { &mut self.0 } +} + +impl io::Write for Ansi { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.0.flush() + } +} + +impl WriteColor for Ansi { + fn supports_color(&self) -> bool { true } + + fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { + try!(self.reset()); + if let Some(ref c) = spec.fg_color { + try!(self.write_color(true, c, spec.bold)); + } + if let Some(ref c) = spec.bg_color { + try!(self.write_color(false, c, spec.bold)); + } + if spec.fg_color.is_none() && spec.bg_color.is_none() { + try!(self.write_str("\x1B[1m")); + } + Ok(()) + } + + fn reset(&mut self) -> io::Result<()> { + self.write_str("\x1B[m") + } +} + +impl Ansi { + fn write_str(&mut self, s: &str) -> io::Result<()> { + self.write_all(s.as_bytes()) + } + + fn write_color( + &mut self, + fg: bool, + c: &Color, + bold: bool, + ) -> io::Result<()> { + // *sigh*... The termion crate doesn't compile on Windows, and we + // need to be able to write ANSI escape sequences on Windows, so I + // guess we have to roll this ourselves. + macro_rules! w { + ($selfie:expr, $fg:expr, $clr:expr) => { + if $fg { + $selfie.write_str(concat!("\x1B[38;5;", $clr, "m")) + } else { + $selfie.write_str(concat!("\x1B[48;5;", $clr, "m")) + } + } + } + if bold { + match *c { + Color::Black => w!(self, fg, "8"), + Color::Blue => w!(self, fg, "12"), + Color::Green => w!(self, fg, "10"), + Color::Red => w!(self, fg, "9"), + Color::Cyan => w!(self, fg, "14"), + Color::Magenta => w!(self, fg, "13"), + Color::Yellow => w!(self, fg, "11"), + Color::White => w!(self, fg, "15"), + Color::__Nonexhaustive => unreachable!(), + } + } else { + match *c { + Color::Black => w!(self, fg, "0"), + Color::Blue => w!(self, fg, "4"), + Color::Green => w!(self, fg, "2"), + Color::Red => w!(self, fg, "1"), + Color::Cyan => w!(self, fg, "6"), + Color::Magenta => w!(self, fg, "5"), + Color::Yellow => w!(self, fg, "3"), + Color::White => w!(self, fg, "7"), + Color::__Nonexhaustive => unreachable!(), + } + } + } +} + +/// An in-memory buffer that provides Windows console coloring. +/// +/// This doesn't actually communicate with the Windows console. Instead, it +/// acts like a normal buffer but also saves the color information associated +/// with positions in the buffer. It is only when the buffer is written to the +/// console that coloring is actually applied. +/// +/// This is roughly isomorphic to the ANSI based approach (i.e., +/// `Ansi>`), except with ANSI, the color information is embedded +/// directly into the buffer. +/// +/// Note that there is no way to write something generic like +/// `WindowsConsole` since coloring on Windows is tied +/// specifically to the console APIs, and therefore can't work on arbitrary +/// writers. +#[cfg(windows)] +#[derive(Clone, Debug)] +struct WindowsBuffer { + /// The actual content that should be printed. + buf: Vec, + /// A sequence of position oriented color specifications. Namely, each + /// element is a position and a color spec, where the color spec should + /// be applied at the position inside of `buf`. + /// + /// A missing color spec implies the underlying console should be reset. + colors: Vec<(usize, Option)>, +} + +#[cfg(windows)] +impl WindowsBuffer { + /// Create a new empty buffer for Windows console coloring. + fn new() -> WindowsBuffer { + WindowsBuffer { + buf: vec![], + colors: vec![], + } + } + + /// Push the given color specification into this buffer. + /// + /// This has the effect of setting the given color information at the + /// current position in the buffer. + fn push(&mut self, spec: Option) { + let pos = self.buf.len(); + self.colors.push((pos, spec)); + } + + /// Print the contents to the given stdout handle, and use the console + /// for coloring. + fn print( + &self, + console: &mut wincolor::Console, + stdout: &mut io::StdoutLock, + ) -> io::Result<()> { + let mut last = 0; + for &(pos, ref spec) in &self.colors { + try!(stdout.write_all(&self.buf[last..pos])); + try!(stdout.flush()); + last = pos; + match *spec { + None => try!(console.reset()), + Some(ref spec) => try!(spec.write_console(console)), + } + } + try!(stdout.write_all(&self.buf[last..])); + stdout.flush() + } + + /// Clear the buffer. + fn clear(&mut self) { + self.buf.clear(); + self.colors.clear(); + } +} + +#[cfg(windows)] +impl io::Write for WindowsBuffer { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.buf.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +#[cfg(windows)] +impl WriteColor for WindowsBuffer { + fn supports_color(&self) -> bool { true } + + fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { + self.push(Some(spec.clone())); + Ok(()) + } + + fn reset(&mut self) -> io::Result<()> { + self.push(None); + Ok(()) + } +} + +/// A color specification. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct ColorSpec { + fg_color: Option, + bg_color: Option, + bold: bool, +} + +impl ColorSpec { + /// Create a new color specification that has no colors or styles. + pub fn new() -> ColorSpec { + ColorSpec { fg_color: None, bg_color: None, bold: false } + } + + /// Get the foreground color. + pub fn fg(&self) -> Option<&Color> { self.fg_color.as_ref() } + + /// Set the foreground color. + pub fn set_fg(&mut self, color: Option) -> &mut ColorSpec { + self.fg_color = color; + self + } + + /// Get the background color. + pub fn bg(&self) -> Option<&Color> { self.bg_color.as_ref() } + + /// Set the background color. + pub fn set_bg(&mut self, color: Option) -> &mut ColorSpec { + self.bg_color = color; + self + } + + /// Get whether this is bold or not. + pub fn bold(&self) -> bool { self.bold } + + /// Set whether the text is bolded or not. + pub fn set_bold(&mut self, yes: bool) -> &mut ColorSpec { + self.bold = yes; + self + } + + /// Clears this color specification so that it has no color/style settings. + pub fn clear(&mut self) { + self.fg_color = None; + self.bg_color = None; + self.bold = false; + } + + /// Writes this color spec to the given Windows console. + #[cfg(windows)] + fn write_console( + &self, + console: &mut wincolor::Console, + ) -> io::Result<()> { + use wincolor::Intense; + + let intense = if self.bold { Intense::Yes } else { Intense::No }; + if let Some(color) = self.fg_color.as_ref().map(|c| c.to_windows()) { + try!(console.fg(intense, color)); + } + if let Some(color) = self.bg_color.as_ref().map(|c| c.to_windows()) { + try!(console.bg(intense, color)); + } + Ok(()) + } +} + +/// The set of available English colors for the terminal foreground/background. +/// +/// Note that this set may expand over time. +#[allow(missing_docs)] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Color { + Black, + Blue, + Green, + Red, + Cyan, + Magenta, + Yellow, + White, + #[doc(hidden)] + __Nonexhaustive, +} + +#[cfg(windows)] +impl Color { + /// Translate this color to a wincolor::Color. + fn to_windows(&self) -> wincolor::Color { + match *self { + Color::Black => wincolor::Color::Black, + Color::Blue => wincolor::Color::Blue, + Color::Green => wincolor::Color::Green, + Color::Red => wincolor::Color::Red, + Color::Cyan => wincolor::Color::Cyan, + Color::Magenta => wincolor::Color::Magenta, + Color::Yellow => wincolor::Color::Yellow, + Color::White => wincolor::Color::White, + Color::__Nonexhaustive => unreachable!(), + } + } +} + +/// An error from parsing an invalid color name. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ParseColorError(String); + +impl ParseColorError { + /// Return the string that couldn't be parsed as a valid color. + pub fn invalid(&self) -> &str { &self.0 } +} + +impl error::Error for ParseColorError { + fn description(&self) -> &str { "unrecognized color name" } +} + +impl fmt::Display for ParseColorError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Unrecognized color name '{}'. Choose from: \ + black, blue, green, red, cyan, magenta, yellow, white.", + self.0) + } +} + +impl FromStr for Color { + type Err = ParseColorError; + + fn from_str(s: &str) -> Result { + match &*s.to_lowercase() { + "black" => Ok(Color::Black), + "blue" => Ok(Color::Blue), + "green" => Ok(Color::Green), + "red" => Ok(Color::Red), + "cyan" => Ok(Color::Cyan), + "magenta" => Ok(Color::Magenta), + "yellow" => Ok(Color::Yellow), + "white" => Ok(Color::White), + _ => Err(ParseColorError(s.to_string())), + } + } +} diff --git a/wincolor/Cargo.toml b/wincolor/Cargo.toml new file mode 100644 index 000000000..226d85b08 --- /dev/null +++ b/wincolor/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "wincolor" +version = "0.1.0" #:version +authors = ["Andrew Gallant "] +description = """ +A simple Windows specific API for controlling text color in a Windows console. +""" +documentation = "https://docs.rs/wincolor" +homepage = "https://github.com/BurntSushi/ripgrep/tree/master/wincolor" +repository = "https://github.com/BurntSushi/ripgrep/tree/master/wincolor" +readme = "README.md" +keywords = ["windows", "win", "color", "ansi", "console"] +license = "Unlicense/MIT" + +[lib] +name = "wincolor" +bench = false + +[dependencies] +kernel32-sys = "0.2.2" +winapi = "0.2.8" diff --git a/wincolor/README.md b/wincolor/README.md new file mode 100644 index 000000000..cc780340e --- /dev/null +++ b/wincolor/README.md @@ -0,0 +1,44 @@ +wincolor +======== +A simple Windows specific API for controlling text color in a Windows console. +The purpose of this crate is to expose the full inflexibility of the Windows +console without any platform independent abstraction. + +[![Windows build status](https://ci.appveyor.com/api/projects/status/github/BurntSushi/ripgrep?svg=true)](https://ci.appveyor.com/project/BurntSushi/ripgrep) +[![](https://img.shields.io/crates/v/wincolor.svg)](https://crates.io/crates/wincolor) + +Dual-licensed under MIT or the [UNLICENSE](http://unlicense.org). + +### Documentation + +[https://docs.rs/wincolor](https://docs.rs/wincolor) + +### Usage + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +wincolor = "0.1" +``` + +and this to your crate root: + +```rust +extern crate wincolor; +``` + +### Example + +This is a simple example that shows how to write text with a foreground color +of cyan and the intense attribute set: + +```rust +use wincolor::{Console, Color, Intense}; + +let mut con = Console::stdout().unwrap(); +con.fg(Intense::Yes, Color::Cyan).unwrap(); +println!("This text will be intense cyan."); +con.reset().unwrap(); +println!("This text will be normal."); +``` diff --git a/wincolor/src/lib.rs b/wincolor/src/lib.rs new file mode 100644 index 000000000..a210b4b26 --- /dev/null +++ b/wincolor/src/lib.rs @@ -0,0 +1,242 @@ +/*! +This crate provides a safe and simple Windows specific API to control +text attributes in the Windows console. Text attributes are limited to +foreground/background colors, as well as whether to make colors intense or not. + +# Example + +```no_run +use wincolor::{Console, Color, Intense}; + +let mut con = Console::stdout().unwrap(); +con.fg(Intense::Yes, Color::Cyan).unwrap(); +println!("This text will be intense cyan."); +con.reset().unwrap(); +println!("This text will be normal."); +``` +*/ +extern crate kernel32; +extern crate winapi; + +use std::io; +use std::mem; + +use winapi::{DWORD, HANDLE, WORD}; +use winapi::winbase::STD_OUTPUT_HANDLE; +use winapi::wincon::{ + FOREGROUND_BLUE as FG_BLUE, + FOREGROUND_GREEN as FG_GREEN, + FOREGROUND_RED as FG_RED, + FOREGROUND_INTENSITY as FG_INTENSITY, +}; + +const FG_CYAN: DWORD = FG_BLUE | FG_GREEN; +const FG_MAGENTA: DWORD = FG_BLUE | FG_RED; +const FG_YELLOW: DWORD = FG_GREEN | FG_RED; +const FG_WHITE: DWORD = FG_BLUE | FG_GREEN | FG_RED; + +/// A Windows console. +/// +/// This represents a very limited set of functionality available to a Windows +/// console. In particular, it can only change text attributes such as color +/// and intensity. +/// +/// There is no way to "write" to this console. Simply write to +/// stdout or stderr instead, while interleaving instructions to the console +/// to change text attributes. +/// +/// A common pitfall when using a console is to forget to flush writes to +/// stdout before setting new text attributes. +#[derive(Debug)] +pub struct Console { + handle: HANDLE, + start_attr: TextAttributes, + cur_attr: TextAttributes, +} + +unsafe impl Send for Console {} + +impl Drop for Console { + fn drop(&mut self) { + unsafe { kernel32::CloseHandle(self.handle); } + } +} + +impl Console { + /// Create a new Console to stdout. + /// + /// If there was a problem creating the console, then an error is returned. + pub fn stdout() -> io::Result { + let mut info = unsafe { mem::zeroed() }; + let (handle, res) = unsafe { + let handle = kernel32::GetStdHandle(STD_OUTPUT_HANDLE); + (handle, kernel32::GetConsoleScreenBufferInfo(handle, &mut info)) + }; + if res == 0 { + return Err(io::Error::last_os_error()); + } + let attr = TextAttributes::from_word(info.wAttributes); + Ok(Console { + handle: handle, + start_attr: attr, + cur_attr: attr, + }) + } + + /// Applies the current text attributes. + fn set(&mut self) -> io::Result<()> { + let attr = self.cur_attr.to_word(); + let res = unsafe { + kernel32::SetConsoleTextAttribute(self.handle, attr) + }; + if res == 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) + } + + /// Apply the given intensity and color attributes to the console + /// foreground. + /// + /// If there was a problem setting attributes on the console, then an error + /// is returned. + pub fn fg(&mut self, intense: Intense, color: Color) -> io::Result<()> { + self.cur_attr.fg_color = color; + self.cur_attr.fg_intense = intense; + self.set() + } + + /// Apply the given intensity and color attributes to the console + /// background. + /// + /// If there was a problem setting attributes on the console, then an error + /// is returned. + pub fn bg(&mut self, intense: Intense, color: Color) -> io::Result<()> { + self.cur_attr.bg_color = color; + self.cur_attr.bg_intense = intense; + self.set() + } + + /// Reset the console text attributes to their original settings. + /// + /// The original settings correspond to the text attributes on the console + /// when this `Console` value was created. + /// + /// If there was a problem setting attributes on the console, then an error + /// is returned. + pub fn reset(&mut self) -> io::Result<()> { + self.cur_attr = self.start_attr; + self.set() + } +} + +/// A representation of text attributes for the Windows console. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +struct TextAttributes { + fg_color: Color, + fg_intense: Intense, + bg_color: Color, + bg_intense: Intense, +} + +impl TextAttributes { + fn to_word(&self) -> WORD { + let mut w = 0; + w |= self.fg_color.to_fg(); + w |= self.fg_intense.to_fg(); + w |= self.bg_color.to_bg(); + w |= self.bg_intense.to_bg(); + w as WORD + } + + fn from_word(word: WORD) -> TextAttributes { + let attr = word as DWORD; + TextAttributes { + fg_color: Color::from_fg(attr), + fg_intense: Intense::from_fg(attr), + bg_color: Color::from_bg(attr), + bg_intense: Intense::from_bg(attr), + } + } +} + +/// Whether to use intense colors or not. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Intense { + Yes, + No, +} + +impl Intense { + fn to_bg(&self) -> DWORD { + self.to_fg() << 4 + } + + fn from_bg(word: DWORD) -> Intense { + Intense::from_fg(word >> 4) + } + + fn to_fg(&self) -> DWORD { + match *self { + Intense::No => 0, + Intense::Yes => FG_INTENSITY, + } + } + + fn from_fg(word: DWORD) -> Intense { + if word & FG_INTENSITY > 0 { + Intense::Yes + } else { + Intense::No + } + } +} + +/// The set of available colors for use with a Windows console. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Color { + Black, + Blue, + Green, + Red, + Cyan, + Magenta, + Yellow, + White, +} + +impl Color { + fn to_bg(&self) -> DWORD { + self.to_fg() << 4 + } + + fn from_bg(word: DWORD) -> Color { + Color::from_fg(word >> 4) + } + + fn to_fg(&self) -> DWORD { + match *self { + Color::Black => 0, + Color::Blue => FG_BLUE, + Color::Green => FG_GREEN, + Color::Red => FG_RED, + Color::Cyan => FG_CYAN, + Color::Magenta => FG_MAGENTA, + Color::Yellow => FG_YELLOW, + Color::White => FG_WHITE, + } + } + + fn from_fg(word: DWORD) -> Color { + match word & 0b111 { + FG_BLUE => Color::Blue, + FG_GREEN => Color::Green, + FG_RED => Color::Red, + FG_CYAN => Color::Cyan, + FG_MAGENTA => Color::Magenta, + FG_YELLOW => Color::Yellow, + FG_WHITE => Color::White, + _ => Color::Black, + } + } +}