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, + } + } +}