From 82317541ec346b85f05a71bd199f16ee13220861 Mon Sep 17 00:00:00 2001 From: DongHun Kwak Date: Thu, 1 Jun 2023 15:47:07 +0900 Subject: [PATCH] Import yansi 0.5.1 --- .cargo_vcs_info.json | 6 + .gitignore | 3 + .travis.yml | 9 + Cargo.toml | 37 ++++ Cargo.toml.orig | 19 ++ LICENSE-APACHE | 201 +++++++++++++++++++++ LICENSE-MIT | 19 ++ README.md | 47 +++++ src/color.rs | 96 +++++++++++ src/lib.rs | 214 +++++++++++++++++++++++ src/macros.rs | 19 ++ src/paint.rs | 463 +++++++++++++++++++++++++++++++++++++++++++++++++ src/style.rs | 480 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/tests.rs | 182 +++++++++++++++++++ src/windows.rs | 69 ++++++++ 15 files changed, 1864 insertions(+) create mode 100644 .cargo_vcs_info.json create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Cargo.toml create mode 100644 Cargo.toml.orig create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 src/color.rs create mode 100644 src/lib.rs create mode 100644 src/macros.rs create mode 100644 src/paint.rs create mode 100644 src/style.rs create mode 100644 src/tests.rs create mode 100644 src/windows.rs diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..070df84 --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "260f62e71fddde4a271174df23a265c7768ac34f" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4308d82 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target/ +**/*.rs.bk +Cargo.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..bfee343 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: rust +matrix: + include: + - rust: stable + script: cargo build --all --all-features --verbose + - rust: beta + script: cargo build --all --all-features --verbose + - rust: nightly + script: cargo test --all --all-features --verbose diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6f03752 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,37 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +name = "yansi" +version = "0.5.1" +authors = ["Sergio Benitez "] +description = "A dead simple ANSI terminal color painting library." +documentation = "https://docs.rs/yansi" +readme = "README.md" +keywords = [ + "ansi", + "terminal", + "color", + "format", + "paint", +] +categories = ["command-line-interface"] +license = "MIT/Apache-2.0" +repository = "https://github.com/SergioBenitez/yansi" + +[dependencies] + +[dev-dependencies.serial_test] +version = "0.6" + +[badges.travis-ci] +branch = "master" +repository = "SergioBenitez/yansi" diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..2ec8a5e --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,19 @@ +[package] +name = "yansi" +version = "0.5.1" +authors = ["Sergio Benitez "] +repository = "https://github.com/SergioBenitez/yansi" +documentation = "https://docs.rs/yansi" +description = "A dead simple ANSI terminal color painting library." +keywords = ["ansi", "terminal", "color", "format", "paint"] +readme = "README.md" +license = "MIT/Apache-2.0" +categories = ["command-line-interface"] + +[badges] +travis-ci = { repository = "SergioBenitez/yansi", branch = "master" } + +[dependencies] + +[dev-dependencies] +serial_test = "0.6" diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..b551a84 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) +Copyright (c) 2017 Sergio Benitez + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..90704d3 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# yansi + +[![Build Status](https://travis-ci.org/SergioBenitez/yansi.svg?branch=master)](https://travis-ci.org/SergioBenitez/yansi) +[![Current Crates.io Version](https://img.shields.io/crates/v/yansi.svg)](https://crates.io/crates/yansi) +[![Documentation](https://docs.rs/yansi/badge.svg)](https://docs.rs/yansi) + +A dead simple ANSI terminal color painting library for Rust. + +```rust +use yansi::Paint; + +print!("{} light, {} light!", Paint::green("Green"), Paint::red("red").underline()); +``` + +See the [documentation](https://docs.rs/yansi/) for more. + +# Why? + +Several terminal coloring libraries exist ([`ansi_term`], [`colored`], +[`term_painter`], to name a few), begging the question: why yet another? Here +are a few reasons: + + * This library is _much_ simpler: there are three types! + * Unlike [`ansi_term`] or [`colored`], _any_ type implementing `Display` + or `Debug` can be stylized, not only strings. + * Styling can be enabled and disabled globally, on the fly. + * Arbitrary items can be [_masked_] for selective disabling. + * Styling can [_wrap_] any arbitrarily styled item. + * Typically only one type needs to be imported: `Paint`. + * Zero dependencies. It really is simple. + * The name `yansi` is pretty short. + +All that being said, this library borrows API ideas from the three libraries as +well as implementation details from [`ansi_term`]. + +[`ansi_term`]: https://crates.io/crates/ansi_term +[`colored`]: https://crates.io/crates/colored +[`term_painter`]: https://crates.io/crates/term-painter +[_masked_]: https://docs.rs/yansi/#masking +[_wrap_]: https://docs.rs/yansi/#wrapping + +## License + +`yansi` is licensed under either of the following, at your option: + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT License ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) diff --git a/src/color.rs b/src/color.rs new file mode 100644 index 0000000..fee620a --- /dev/null +++ b/src/color.rs @@ -0,0 +1,96 @@ +use std::fmt; + +use {Paint, Style}; + +/// An enum representing an ANSI color code. +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Copy, Clone)] +pub enum Color { + /// No color has been set. Nothing is changed when applied. + Unset, + + /// Terminal default #9. (foreground code `39`, background code `49`). + Default, + + /// Black #0 (foreground code `30`, background code `40`). + Black, + + /// Red: #1 (foreground code `31`, background code `41`). + Red, + + /// Green: #2 (foreground code `32`, background code `42`). + Green, + + /// Yellow: #3 (foreground code `33`, background code `43`). + Yellow, + + /// Blue: #4 (foreground code `34`, background code `44`). + Blue, + + /// Magenta: #5 (foreground code `35`, background code `45`). + Magenta, + + /// Cyan: #6 (foreground code `36`, background code `46`). + Cyan, + + /// White: #7 (foreground code `37`, background code `47`). + White, + + /// A color number from 0 to 255, for use in 256-color terminals. + Fixed(u8), + + /// A 24-bit RGB color, as specified by ISO-8613-3. + RGB(u8, u8, u8), +} + +impl Color { + /// Constructs a new `Paint` structure that encapsulates `item` with the + /// foreground color set to the color `self`. + /// + /// ```rust + /// use yansi::Color::Blue; + /// + /// println!("This is going to be blue: {}", Blue.paint("yay!")); + /// ``` + #[inline] + pub fn paint(self, item: T) -> Paint { + Paint::new(item).fg(self) + } + + /// Constructs a new `Style` structure with the foreground color set to the + /// color `self`. + /// + /// ```rust + /// use yansi::Color::Green; + /// + /// let success = Green.style().bold(); + /// println!("Hey! {}", success.paint("Success!")); + /// ``` + #[inline] + pub fn style(self) -> Style { + Style::new(self) + } + + pub(crate) fn ascii_fmt(&self, f: &mut fmt::Write) -> fmt::Result { + match *self { + Color::Unset => Ok(()), + Color::Default => write!(f, "9"), + Color::Black => write!(f, "0"), + Color::Red => write!(f, "1"), + Color::Green => write!(f, "2"), + Color::Yellow => write!(f, "3"), + Color::Blue => write!(f, "4"), + Color::Magenta => write!(f, "5"), + Color::Cyan => write!(f, "6"), + Color::White => write!(f, "7"), + Color::Fixed(num) => write!(f, "8;5;{}", num), + Color::RGB(r, g, b) => write!(f, "8;2;{};{};{}", r, g, b), + } + } +} + +impl Default for Color { + #[inline(always)] + fn default() -> Self { + Color::Unset + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8fccd80 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,214 @@ +#![doc(html_root_url = "https://docs.rs/yansi/0.6.0-dev")] + +//! A dead simple ANSI terminal color painting library. +//! +//! # Usage +//! +//! Usage is best illustrated via a quick example: +//! +//! ```rust +//! use yansi::{Paint, Color}; +//! +//! println!("Testing, {}, {}, {}!", +//! Paint::red(1), +//! Paint::green(2).bold().underline(), +//! Paint::blue("3").bg(Color::White).italic()); +//! ``` +//! +//! ## Paint +//! +//! The main entry point into this library is the [`Paint`] type. `Paint` +//! encapsulates a value of any type that implements the [`Display`] or +//! [`Debug`] trait. When a `Paint` is `Display`ed or `Debug`ed, the appropriate +//! ANSI escape characters are emitted before and after the wrapped type's `fmt` +//! implementation. +//! +//! `Paint` can be constructed via [a myriad of methods]. In addition to these +//! constructors, you can also use the [`color.paint()`](Color::paint()) method +//! on a given [`Color`] value to construct a `Paint` type. Both of these +//! approaches are shown below: +//! +//! ```rust +//! use yansi::Paint; +//! use yansi::Color::Red; +//! +//! println!("I'm {}!", Paint::red("red").bold()); +//! println!("I'm also {}!", Red.paint("red").bold()); +//! ``` +//! [`Display`]: ::std::fmt::Display +//! [`Debug`]: ::std::fmt::Debug +//! [a myriad of methods]: struct.Paint.html#unstyled-constructors +//! +//! ## Styling +//! +//! Modifications to the styling of an item can be made via [a number of +//! chainable methods] on `Paint`. +//! +//! ```rust +//! use yansi::Paint; +//! +//! Paint::new("hi").underline().invert().italic().dimmed().bold(); +//! ``` +//! +//! Styling can also be created independently from a `Paint` structure via the +//! [`Style`] structure. This allows common styling to be stored and reused. A +//! `Style` can be applied via the [`style.paint()`] method or the +//! [`paint.with_style()`] method: +//! +//! ```rust +//! use yansi::{Paint, Color, Style}; +//! +//! // A bold, itatlic style with red foreground. +//! let alert = Style::new(Color::Red).bold().italic(); +//! +//! // Using `style.paint()`; this is preferred. +//! println!("Alert! {}", alert.paint("This is serious business!")); +//! println!("Hi! {}", alert.underline().paint("Super serious!")); +//! +//! // Using `paint.with_style()`. +//! println!("Alert! {}", Paint::new("Yet another.").with_style(alert)); +//! ``` +//! +//! [a number of chainable methods]: struct.Paint.html#setters +//! [`style.paint()`]: Style::paint() +//! [`paint.with_style()`]: Paint::with_style() +//! +//! # Disabling +//! +//! Painting can be disabled globally via the [`Paint::disable()`] method. When +//! painting is disabled, the `Display` and `Debug` implementations for `Paint` +//! will emit the `Display` or `Debug` of the contained object and nothing else. +//! Painting can be reenabled via the [`Paint::enable()`] method. +//! +//! One potential use of this feature is to allow users to control color ouput +//! via an environment variable. For instance, to disable coloring if the +//! `CLICOLOR` variable is set to `0`, you might write: +//! +//! ```rust +//! # { if false { // we don't actually want to disable coloring +//! use yansi::Paint; +//! +//! if let Ok(true) = std::env::var("CLICOLOR").map(|v| v == "0") { +//! Paint::disable(); +//! } +//! # } } +//! ``` +//! +//! ## Masking +//! +//! Items can be arbitrarily _masked_. When an item is masked and painting is +//! disabled, the `Display` and `Debug` implementations of `Paint` write +//! nothing. This allows you to selectively omit output when painting is +//! disabled. Values can be masked using the [`Paint::masked()`] constructor +//! or [`paint.mask()`] and [`style.mask()`] style setters. +//! +//! [`paint.mask()`]: Paint::mask() +//! [`style.mask()`]: Style::mask() +//! +//! One use for this feature is to print certain characters only when painting +//! is enabled. For instance, you might wish to emit the 🎨 emoji when +//! coloring is enabled but not otherwise. This can be accomplished by masking +//! the emoji: +//! +//! ```rust +//! use yansi::Paint; +//! +//! println!("I like colors!{}", Paint::masked(" 🎨")); +//! ``` +//! +//! This will print "I like colors! 🎨" when painting is enabled and "I like +//! colors!" when painting is disabled. +//! +//! ## Wrapping +//! +//! Styling can be set to _wrap_ existing styles using either the +//! [`Paint::wrapping()`] constructor or the [`paint.wrap()`] and +//! [`style.wrap()`] style setters. When a style is _wrapping_, all color +//! resets written out by the internal item's `Display` or `Debug` +//! implementation are set to the styling of the wrapping style itself. In other +//! words, the "default" style of the wrapped item is modified to be the +//! wrapping style. This allows for easy wrapping of other colored text. Without +//! this feature, the console would reset styling to the terminal's default +//! style instead of the wrapping style. +//! +//! [`paint.wrap()`]: Paint::wrap() +//! [`style.wrap()`]: Style::wrap() +//! +//! One use for this feature is to ensure that styling is consistently set +//! across items that may already be styled, such as when logging. +//! +//! ```rust +//! use yansi::{Paint, Color}; +//! +//! let inner = format!("{} and {}", Paint::red("Stop"), Paint::green("Go")); +//! println!("Hey! {}", Paint::wrapping(inner).fg(Color::Blue)); +//! ``` +//! +//! This will print 'Hey!' unstyled, "Stop" in red, "and" in blue, and "Go" in +//! green. Without wrapping, "and" would be unstyled as `Paint::red()` resets +//! the style after printing the internal item. +//! +//! # Windows +//! +//! Coloring is supported on Windows beginning with the Windows 10 anniversary +//! update. Since this update, Windows consoles support ANSI escape sequences. +//! This support, however, must be explicitly enabled. `yansi` provides the +//! [`Paint::enable_windows_ascii()`] method to enable ASCII support on Windows +//! consoles when available. +//! +//! ```rust +//! use yansi::Paint; +//! +//! // Enable ASCII escape sequence support on Windows consoles. +//! Paint::enable_windows_ascii(); +//! ``` +//! +//! You may wish to disable coloring on unsupported Windows consoles to avoid +//! emitting unrecognized ASCII escape sequences: +//! +//! ```rust +//! use yansi::Paint; +//! +//! if cfg!(windows) && !Paint::enable_windows_ascii() { +//! Paint::disable(); +//! } +//! ``` +//! +//! [`Paint::enable_windows_ascii()`]: Paint::enable_windows_ascii() +//! +//! # Why? +//! +//! Several terminal coloring libraries exist ([`ansi_term`], [`colored`], +//! [`term_painter`], to name a few), begging the question: why yet another? +//! Here are a few reasons: +//! +//! * This library is _much_ simpler: there are three types! +//! * Unlike [`ansi_term`] or [`colored`], _any_ type implementing `Display` +//! or `Debug` can be stylized, not only strings. +//! * Styling can be enabled and disabled globally, on the fly. +//! * Arbitrary items can be [_masked_] for selective disabling. +//! * Styling can [_wrap_] any arbitrarily styled item. +//! * Typically only one type needs to be imported: [`Paint`]. +//! * Zero dependencies. It really is simple. +//! * The name `yansi` is pretty short. +//! +//! All that being said, this library borrows API ideas from the three libraries +//! as well as implementation details from [`ansi_term`]. +//! +//! [`ansi_term`]: https://crates.io/crates/ansi_term +//! [`colored`]: https://crates.io/crates/colored +//! [`term_painter`]: https://crates.io/crates/term-painter +//! [_masked_]: #masking +//! [_wrap_]: #wrapping + +#[macro_use] mod macros; + +#[cfg(test)] mod tests; +mod windows; +mod paint; +mod style; +mod color; + +pub use color::Color; +pub use style::Style; +pub use paint::Paint; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..b7c0b03 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,19 @@ +macro_rules! style_builder_for { + ($T:ty, |$s:ident| $props:expr, $($name:ident: $property:ident),*) => ($( + #[doc = concat!( + "Enables the _", stringify!($name), "_ style on `self`.\n", + "```rust\n", + "use yansi::Paint;\n", + "\n", + "println!(\"Using ", stringify!($name), ": {}\", ", + "Paint::new(\"hi\").", stringify!($name), "());\n", + "```\n" + )] + #[inline] + pub fn $name(self) -> $T { + let mut $s = self; + $props.set(Property::$property); + $s + } + )*) +} diff --git a/src/paint.rs b/src/paint.rs new file mode 100644 index 0000000..9ead595 --- /dev/null +++ b/src/paint.rs @@ -0,0 +1,463 @@ +use std::fmt; + +use style::{Style, Property}; +use color::Color; + +/// A structure encapsulating an item and styling. +/// +/// See the [crate level documentation](./) for usage information. +/// +/// # Method Glossary +/// +/// The `Paint` structure exposes many methods for convenience. +/// +/// ### Unstyled Constructors +/// +/// Return a new `Paint` structure with no or default styling applied. +/// +/// * [`Paint::new(item: T)`](Paint::new()) +/// * [`Paint::default(item: T)`](Paint::default()) +/// * [`Paint::masked(item: T)`](Paint::masked()) +/// * [`Paint::wrapping(item: T)`](Paint::wrapping()) +/// +/// ### Foreground Color Constructors +/// +/// Return a new `Paint` structure with a foreground color applied. +/// +/// * [`Paint::rgb(r: u8, g: u8, b: u8, item: T)`](Paint::rgb()) +/// * [`Paint::fixed(color: u8, item: T)`](Paint::fixed()) +/// * [`Paint::black(item: T)`](Paint::black()) +/// * [`Paint::red(item: T)`](Paint::red()) +/// * [`Paint::green(item: T)`](Paint::green()) +/// * [`Paint::yellow(item: T)`](Paint::yellow()) +/// * [`Paint::blue(item: T)`](Paint::blue()) +/// * [`Paint::magenta(item: T)`](Paint::magenta()) +/// * [`Paint::cyan(item: T)`](Paint::cyan()) +/// * [`Paint::white(item: T)`](Paint::white()) +/// +/// ### Getters +/// +/// Return information about the `Paint` structure. +/// +/// * [`paint.style()`](Paint::style()) +/// * [`paint.inner()`](Paint::inner()) +/// +/// ### Setters +/// +/// Set a style property on a given `Paint` structure. +/// +/// * [`paint.with_style(style: Style)`](Paint::with_style()) +/// * [`paint.mask()`](Paint::mask()) +/// * [`paint.wrap()`](Paint::wrap()) +/// * [`paint.fg(color: Color)`](Paint::fg()) +/// * [`paint.bg(color: Color)`](Paint::bg()) +/// * [`paint.bold()`](Paint::bold()) +/// * [`paint.dimmed()`](Paint::dimmed()) +/// * [`paint.italic()`](Paint::italic()) +/// * [`paint.underline()`](Paint::underline()) +/// * [`paint.blink()`](Paint::blink()) +/// * [`paint.invert()`](Paint::invert()) +/// * [`paint.hidden()`](Paint::hidden()) +/// * [`paint.strikethrough()`](Paint::strikethrough()) +/// +/// These methods can be chained: +/// +/// ```rust +/// use yansi::Paint; +/// +/// Paint::new("hi").underline().invert().italic().dimmed().bold(); +/// ``` +/// +/// ### Global Methods +/// +/// Modify or observe the global behavior of painting. +/// +/// * [`Paint::enable()`](Paint::enable()) +/// * [`Paint::disable()`](Paint::disable()) +/// * [`Paint::is_enabled()`](Paint::is_enabled()) +/// * [`Paint::enable_windows_ascii()`](Paint::enable_windows_ascii()) +#[derive(Default, Eq, PartialEq, Ord, PartialOrd, Hash, Copy, Clone)] +pub struct Paint { + item: T, + style: Style, +} + +macro_rules! constructors_for { + ($T:ty, $($name:ident: $color:ident),*) => ($( + #[doc = concat!( + "Constructs a new `Paint` structure encapsulating `item` with the foreground color\n", + "set to ", stringify!($name), ".\n", + "```rust\n", + "use yansi::Paint;\n", + "\n", + "println!(\"This is going to be ", stringify!($name), + ": {}\", Paint::", stringify!($name), "(\"yay!\"));\n", + "```\n" + )] + #[inline] + pub fn $name(item: $T) -> Paint<$T> { + Paint::new(item).fg(Color::$color) + } + )*) +} + +impl Paint { + /// Constructs a new `Paint` structure encapsulating `item` with no set + /// styling. + /// + /// ```rust + /// use yansi::Paint; + /// + /// assert_eq!(Paint::new("hello!").to_string(), "hello!".to_string()); + /// ``` + #[inline] + pub fn new(item: T) -> Paint { + Paint { item, style: Style::default() } + } + + /// Constructs a new `Paint` structure encapsulating `item` with the active + /// terminal's default foreground and background. + /// + /// ```rust + /// use yansi::Paint; + /// + /// println!("This is going to use {}!", Paint::default("default colors")); + /// ``` + #[inline] + pub fn default(item: T) -> Paint { + Paint::new(item).fg(Color::Default).bg(Color::Default) + } + + /// Constructs a new _masked_ `Paint` structure encapsulating `item` with + /// no set styling. + /// + /// A masked `Paint` is not written out when painting is disabled during + /// `Display` or `Debug` invocations. When painting is enabled, masking has + /// no effect. + /// + /// ```rust + /// use yansi::Paint; + /// + /// // The emoji won't be printed when coloring is disabled. + /// println!("{}Sprout!", Paint::masked("🌱 ")); + /// ``` + #[inline] + pub fn masked(item: T) -> Paint { + Paint::new(item).mask() + } + + /// Constructs a new _wrapping_ `Paint` structure encapsulating `item` with + /// default styling. + /// + /// A wrapping `Paint` converts all color resets written out by the internal + /// value to the styling of itself. This allows for seamless color wrapping + /// of other colored text. + /// + /// # Performance + /// + /// In order to wrap an internal value, the internal value must first be + /// written out to a local buffer and examined. As a result, displaying a + /// wrapped value is likely to result in a heap allocation and copy. + /// + /// # Example + /// + /// ```rust + /// use yansi::{Paint, Color}; + /// + /// let inner = format!("{} and {}", Paint::red("Stop"), Paint::green("Go")); + /// + /// // 'Hey!' will be unstyled, "Stop" will be red, "and" will be blue, and + /// // "Go" will be green. Without a wrapping `Paint`, "and" would be + /// // unstyled. + /// println!("Hey! {}", Paint::wrapping(inner).fg(Color::Blue)); + /// ``` + #[inline] + pub fn wrapping(item: T) -> Paint { + Paint::new(item).wrap() + } + + /// Constructs a new `Paint` structure encapsulating `item` with the + /// foreground color set to the RGB color `r`, `g`, `b`. + /// + /// ```rust + /// use yansi::Paint; + /// + /// println!("This is going to be funky: {}", Paint::rgb(70, 130, 122, "hi!")); + /// ``` + #[inline] + pub fn rgb(r: u8, g: u8, b: u8, item: T) -> Paint { + Paint::new(item).fg(Color::RGB(r, g, b)) + } + + /// Constructs a new `Paint` structure encapsulating `item` with the + /// foreground color set to the fixed 8-bit color `color`. + /// + /// ```rust + /// use yansi::Paint; + /// + /// println!("This is going to be funky: {}", Paint::fixed(100, "hi!")); + /// ``` + #[inline] + pub fn fixed(color: u8, item: T) -> Paint { + Paint::new(item).fg(Color::Fixed(color)) + } + + constructors_for!(T, black: Black, red: Red, green: Green, yellow: Yellow, + blue: Blue, magenta: Magenta, cyan: Cyan, white: White); + + /// Retrieves the style currently set on `self`. + /// + /// ```rust + /// use yansi::{Style, Color, Paint}; + /// + /// let alert = Style::new(Color::Red).bold().underline(); + /// let painted = Paint::red("hi").bold().underline(); + /// + /// assert_eq!(alert, painted.style()); + /// ``` + #[inline] + pub fn style(&self) -> Style { + self.style + } + + /// Retrieves a borrow to the inner item. + /// + /// ```rust + /// use yansi::Paint; + /// + /// let x = Paint::red("Hello, world!"); + /// assert_eq!(*x.inner(), "Hello, world!"); + /// ``` + #[inline] + pub fn inner(&self) -> &T { + &self.item + } + + /// Sets the style of `self` to `style`. + /// + /// Any styling currently set on `self` is lost. Prefer to use the + /// [`style.paint()`](Style::paint()) method to create a `Paint` struct from + /// `Style`. + /// + /// ```rust + /// use yansi::{Paint, Color, Style}; + /// + /// let s = Style::new(Color::Red).bold().underline(); + /// + /// // Using this method. + /// println!("Alert: {}", Paint::new("This thing happened!").with_style(s)); + /// + /// // Using the `style.paint()` method. + /// println!("Alert: {}", s.paint("This thing happened!")); + /// ``` + #[inline] + pub fn with_style(mut self, style: Style) -> Paint { + self.style = style; + self + } + + /// Masks `self`. + /// + /// A masked `Paint` is not written out when painting is disabled during + /// `Display` or `Debug` invocations. When painting is enabled, masking has + /// no effect. + /// + /// ```rust + /// use yansi::Paint; + /// + /// // "Whoops! " will only print when coloring is enabled. + /// println!("{}Something happened.", Paint::red("Whoops! ").mask()); + /// ``` + #[inline] + pub fn mask(mut self) -> Paint { + self.style.masked = true; + self + } + + /// Makes `self` a _wrapping_ `Paint`. + /// + /// A wrapping `Paint` converts all color resets written out by the internal + /// value to the styling of itself. This allows for seamless color wrapping + /// of other colored text. + /// + /// # Performance + /// + /// In order to wrap an internal value, the internal value must first be + /// written out to a local buffer and examined. As a result, displaying a + /// wrapped value is likely to result in a heap allocation and copy. + /// + /// # Example + /// + /// ```rust + /// use yansi::{Paint, Color}; + /// + /// let inner = format!("{} and {}", Paint::red("Stop"), Paint::green("Go")); + /// + /// // 'Hey!' will be unstyled, "Stop" will be red, "and" will be blue, and + /// // "Go" will be green. Without a wrapping `Paint`, "and" would be + /// // unstyled. + /// println!("Hey! {}", Paint::blue(inner).wrap()); + /// ``` + #[inline] + pub fn wrap(mut self) -> Paint { + self.style.wrap = true; + self + } + + /// Sets the foreground to `color`. + /// + /// ```rust + /// use yansi::Paint; + /// use yansi::Color::Red; + /// + /// println!("Red foreground: {}", Paint::new("hi!").fg(Red)); + /// ``` + #[inline] + pub fn fg(mut self, color: Color) -> Paint { + self.style.foreground = color; + self + } + + /// Sets the background to `color`. + /// + /// ```rust + /// use yansi::Paint; + /// use yansi::Color::Yellow; + /// + /// println!("Yellow background: {}", Paint::new("hi!").bg(Yellow)); + /// ``` + #[inline] + pub fn bg(mut self, color: Color) -> Paint { + self.style.background = color; + self + } + + style_builder_for!(Paint, |paint| paint.style.properties, + bold: BOLD, dimmed: DIMMED, italic: ITALIC, + underline: UNDERLINE, blink: BLINK, invert: INVERT, + hidden: HIDDEN, strikethrough: STRIKETHROUGH); +} + +macro_rules! impl_fmt_trait { + ($trait:ident, $fmt:expr) => ( + impl fmt::$trait for Paint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if Paint::is_enabled() && self.style.wrap { + let mut prefix = String::new(); + prefix.push_str("\x1B[0m"); + self.style.fmt_prefix(&mut prefix)?; + + self.style.fmt_prefix(f)?; + let item = format!($fmt, self.item).replace("\x1B[0m", &prefix); + fmt::$trait::fmt(&item, f)?; + self.style.fmt_suffix(f) + } else if Paint::is_enabled() { + self.style.fmt_prefix(f)?; + fmt::$trait::fmt(&self.item, f)?; + self.style.fmt_suffix(f) + } else if !self.style.masked { + fmt::$trait::fmt(&self.item, f) + } else { + Ok(()) + } + } + } + ) +} + +impl_fmt_trait!(Display, "{}"); +impl_fmt_trait!(Debug, "{:?}"); + +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; + +static ENABLED: AtomicBool = AtomicBool::new(true); + +impl Paint<()> { + /// Disables coloring globally. + /// + /// # Example + /// + /// ```rust + /// use yansi::Paint; + /// + /// // With coloring enabled, ANSI color codes are emitted. + /// assert_ne!(Paint::green("go").to_string(), "go".to_string()); + /// + /// // With coloring disabled, ANSI color codes are _not_ emitted. + /// Paint::disable(); + /// assert_eq!(Paint::green("go").to_string(), "go".to_string()); + /// ``` + pub fn disable() { + ENABLED.store(false, Ordering::Release); + } + + /// Enables coloring globally. Coloring is enabled by default, so this + /// method should only be called to _re_ enable coloring. + /// + /// # Example + /// + /// ```rust + /// use yansi::Paint; + /// + /// // With coloring disabled, ANSI color codes are _not_ emitted. + /// Paint::disable(); + /// assert_eq!(Paint::green("go").to_string(), "go".to_string()); + /// + /// // Reenabling causes color code to be emitted. + /// Paint::enable(); + /// assert_ne!(Paint::green("go").to_string(), "go".to_string()); + /// ``` + pub fn enable() { + ENABLED.store(true, Ordering::Release); + } + + /// Returns `true` if coloring is enabled and `false` otherwise. Coloring is + /// enabled by default but can be enabled and disabled on-the-fly with the + /// [`Paint::enable()`] and [`Paint::disable()`] methods. + /// + /// [`Paint::disable()`]: struct.Paint.html#method.disable + /// [`Paint::enable()`]: struct.Paint.html#method.disable + /// + /// # Example + /// + /// ```rust + /// use yansi::Paint; + /// + /// // Coloring is enabled by default. + /// assert!(Paint::is_enabled()); + /// + /// // Disable it with `Paint::disable()`. + /// Paint::disable(); + /// assert!(!Paint::is_enabled()); + /// + /// // Reenable with `Paint::enable()`. + /// Paint::enable(); + /// assert!(Paint::is_enabled()); + /// ``` + pub fn is_enabled() -> bool { + ENABLED.load(Ordering::Acquire) + } + + /// Enables ASCII terminal escape sequences on Windows consoles when + /// possible. Returns `true` if escape sequence support was successfully + /// enabled and `false` otherwise. On non-Windows targets, this method + /// always returns `true`. + /// + /// Support for escape sequences in Windows consoles was added in the + /// Windows 10 anniversary update. For targets with older Windows + /// installations, this method is expected to return `false`. + /// + /// # Example + /// + /// ```rust + /// use yansi::Paint; + /// + /// // A best-effort Windows ASCII terminal support enabling. + /// Paint::enable_windows_ascii(); + /// ``` + #[inline] + pub fn enable_windows_ascii() -> bool { + ::windows::enable_ascii_colors() + } +} diff --git a/src/style.rs b/src/style.rs new file mode 100644 index 0000000..3c4f49a --- /dev/null +++ b/src/style.rs @@ -0,0 +1,480 @@ +use std::hash::{Hash, Hasher}; +use std::fmt::{self, Display}; +use std::ops::BitOr; + +use {Paint, Color}; + +#[derive(Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Copy, Clone)] +pub struct Property(u8); + +impl Property { + pub const BOLD: Self = Property(1 << 0); + pub const DIMMED: Self = Property(1 << 1); + pub const ITALIC: Self = Property(1 << 2); + pub const UNDERLINE: Self = Property(1 << 3); + pub const BLINK: Self = Property(1 << 4); + pub const INVERT: Self = Property(1 << 5); + pub const HIDDEN: Self = Property(1 << 6); + pub const STRIKETHROUGH: Self = Property(1 << 7); + + #[inline(always)] + pub fn contains(self, other: Property) -> bool { + (other.0 & self.0) == other.0 + } + + #[inline(always)] + pub fn set(&mut self, other: Property) { + self.0 |= other.0; + } + + #[inline(always)] + pub fn iter(self) -> Iter { + Iter { index: 0, properties: self } + } +} + +impl BitOr for Property { + type Output = Self; + + #[inline(always)] + fn bitor(self, rhs: Self) -> Self { + Property(self.0 | rhs.0) + } +} + +pub struct Iter { + index: u8, + properties: Property, +} + +impl Iterator for Iter { + type Item = usize; + + fn next(&mut self) -> Option { + while self.index < 8 { + let index = self.index; + self.index += 1; + + if self.properties.contains(Property(1 << index)) { + return Some(index as usize); + } + } + + None + } +} + +/// Represents a set of styling options. +/// +/// See the [crate level documentation](./) for usage information. +/// +/// # Method Glossary +/// +/// The `Style` structure exposes many methods for convenience. The majority of +/// these methods are shared with [`Paint`](Paint). +/// +/// ### Foreground Color Constructors +/// +/// Return a new `Style` structure with a foreground `color` applied. +/// +/// * [`Style::new(color: Color)`](Style::new()) +/// +/// ### Setters +/// +/// Set a style property on a given `Style` structure. +/// +/// * [`style.fg(color: Color)`](Style::fg()) +/// * [`style.bg(color: Color)`](Style::bg()) +/// * [`style.mask()`](Style::mask()) +/// * [`style.wrap()`](Style::wrap()) +/// * [`style.bold()`](Style::bold()) +/// * [`style.dimmed()`](Style::dimmed()) +/// * [`style.italic()`](Style::italic()) +/// * [`style.underline()`](Style::underline()) +/// * [`style.blink()`](Style::blink()) +/// * [`style.invert()`](Style::invert()) +/// * [`style.hidden()`](Style::hidden()) +/// * [`style.strikethrough()`](Style::strikethrough()) +/// +/// These methods can be chained: +/// +/// ```rust +/// use yansi::{Style, Color::{Red, Magenta}}; +/// +/// Style::new(Red).bg(Magenta).underline().invert().italic().dimmed().bold(); +/// ``` +/// +/// ### Converters +/// +/// Convert a `Style` into another structure. +/// +/// * [`style.paint(item: T) -> Paint`](Style::paint()) +/// +/// ### Getters +/// +/// Return information about a `Style` structure. +/// +/// * [`style.fg_color()`](Style::fg_color()) +/// * [`style.bg_color()`](Style::bg_color()) +/// * [`style.is_masked()`](Style::is_masked()) +/// * [`style.is_wrapping()`](Style::is_wrapping()) +/// * [`style.is_bold()`](Style::is_bold()) +/// * [`style.is_dimmed()`](Style::is_dimmed()) +/// * [`style.is_italic()`](Style::is_italic()) +/// * [`style.is_underline()`](Style::is_underline()) +/// * [`style.is_blink()`](Style::is_blink()) +/// * [`style.is_invert()`](Style::is_invert()) +/// * [`style.is_hidden()`](Style::is_hidden()) +/// * [`style.is_strikethrough()`](Style::is_strikethrough()) +/// +/// ### Raw Formatters +/// +/// Write the raw ANSI codes for a given `Style` to any `fmt::Write`. +/// +/// * [`style.fmt_prefix(f: &mut fmt::Write)`](Style::fmt_prefix()) +/// * [`style.fmt_suffix(f: &mut fmt::Write)`](Style::fmt_suffix()) +#[repr(packed)] +#[derive(Default, Debug, Eq, Ord, PartialOrd, Copy, Clone)] +pub struct Style { + pub(crate) foreground: Color, + pub(crate) background: Color, + pub(crate) properties: Property, + pub(crate) masked: bool, + pub(crate) wrap: bool, +} + +impl PartialEq for Style { + fn eq(&self, other: &Style) -> bool { + self.foreground == other.foreground + && self.background == other.background + && self.properties == other.properties + } +} + +impl Hash for Style { + fn hash(&self, state: &mut H) { + self.foreground.hash(state); + self.background.hash(state); + self.properties.hash(state); + } +} + +macro_rules! checker_for { + ($($name:ident ($fn_name:ident): $property:ident),*) => ($( + #[doc = concat!( + "Returns `true` if the _", stringify!($name), "_ property is set on `self`.\n", + "```rust\n", + "use yansi::Style;\n", + "\n", + "let plain = Style::default();\n", + "assert!(!plain.", stringify!($fn_name), "());\n", + "\n", + "let styled = plain.", stringify!($name), "();\n", + "assert!(styled.", stringify!($fn_name), "());\n", + "```\n" + )] + #[inline] + pub fn $fn_name(&self) -> bool { + self.properties.contains(Property::$property) + } + )*) +} + +#[inline] +fn write_spliced(c: &mut bool, f: &mut fmt::Write, t: T) -> fmt::Result { + if *c { + write!(f, ";{}", t) + } else { + *c = true; + write!(f, "{}", t) + } +} + +impl Style { + /// Default style with the foreground set to `color` and no other set + /// properties. + /// + /// ```rust + /// use yansi::Style; + /// + /// let plain = Style::default(); + /// assert_eq!(plain, Style::default()); + /// ``` + #[inline] + pub fn new(color: Color) -> Style { + Self::default().fg(color) + } + + /// Sets the foreground to `color`. + /// + /// ```rust + /// use yansi::{Color, Style}; + /// + /// let red_fg = Style::default().fg(Color::Red); + /// ``` + #[inline] + pub fn fg(mut self, color: Color) -> Style { + self.foreground = color; + self + } + + /// Sets the background to `color`. + /// + /// ```rust + /// use yansi::{Color, Style}; + /// + /// let red_bg = Style::default().bg(Color::Red); + /// ``` + #[inline] + pub fn bg(mut self, color: Color) -> Style { + self.background = color; + self + } + + /// Sets `self` to be masked. + /// + /// An item with _masked_ styling is not written out when painting is + /// disabled during `Display` or `Debug` invocations. When painting is + /// enabled, masking has no effect. + /// + /// ```rust + /// use yansi::Style; + /// + /// let masked = Style::default().mask(); + /// + /// // "Whoops! " will only print when coloring is enabled. + /// println!("{}Something happened.", masked.paint("Whoops! ")); + /// ``` + #[inline] + pub fn mask(mut self) -> Style { + self.masked = true; + self + } + + /// Sets `self` to be wrapping. + /// + /// A wrapping `Style` converts all color resets written out by the internal + /// value to the styling of itself. This allows for seamless color wrapping + /// of other colored text. + /// + /// # Performance + /// + /// In order to wrap an internal value, the internal value must first be + /// written out to a local buffer and examined. As a result, displaying a + /// wrapped value is likely to result in a heap allocation and copy. + /// + /// ```rust + /// use yansi::{Paint, Style, Color}; + /// + /// let inner = format!("{} and {}", Paint::red("Stop"), Paint::green("Go")); + /// let wrapping = Style::new(Color::Blue).wrap(); + /// + /// // 'Hey!' will be unstyled, "Stop" will be red, "and" will be blue, and + /// // "Go" will be green. Without a wrapping `Paint`, "and" would be + /// // unstyled. + /// println!("Hey! {}", wrapping.paint(inner)); + /// ``` + #[inline] + pub fn wrap(mut self) -> Style { + self.wrap = true; + self + } + + style_builder_for!(Style, |style| style.properties, + bold: BOLD, dimmed: DIMMED, italic: ITALIC, + underline: UNDERLINE, blink: BLINK, invert: INVERT, + hidden: HIDDEN, strikethrough: STRIKETHROUGH); + + /// Constructs a new `Paint` structure that encapsulates `item` with the + /// style set to `self`. + /// + /// ```rust + /// use yansi::{Style, Color}; + /// + /// let alert = Style::new(Color::Red).bold().underline(); + /// println!("Alert: {}", alert.paint("This thing happened!")); + /// ``` + #[inline] + pub fn paint(self, item: T) -> Paint { + Paint::new(item).with_style(self) + } + + /// Returns the foreground color of `self`. + /// + /// ```rust + /// use yansi::{Style, Color}; + /// + /// let plain = Style::default(); + /// assert_eq!(plain.fg_color(), Color::Unset); + /// + /// let red = plain.fg(Color::Red); + /// assert_eq!(red.fg_color(), Color::Red); + /// ``` + #[inline] + pub fn fg_color(&self) -> Color { + self.foreground + } + + /// Returns the foreground color of `self`. + /// + /// ```rust + /// use yansi::{Style, Color}; + /// + /// let plain = Style::default(); + /// assert_eq!(plain.bg_color(), Color::Unset); + /// + /// let white = plain.bg(Color::White); + /// assert_eq!(white.bg_color(), Color::White); + /// ``` + #[inline] + pub fn bg_color(&self) -> Color { + self.background + } + + /// Returns `true` if `self` is masked. + /// + /// ```rust + /// use yansi::Style; + /// + /// let plain = Style::default(); + /// assert!(!plain.is_masked()); + /// + /// let masked = plain.mask(); + /// assert!(masked.is_masked()); + /// ``` + #[inline] + pub fn is_masked(&self) -> bool { + self.masked + } + + /// Returns `true` if `self` is wrapping. + /// + /// ```rust + /// use yansi::Style; + /// + /// let plain = Style::default(); + /// assert!(!plain.is_wrapping()); + /// + /// let wrapping = plain.wrap(); + /// assert!(wrapping.is_wrapping()); + /// ``` + #[inline] + pub fn is_wrapping(&self) -> bool { + self.wrap + } + + checker_for!(bold (is_bold): BOLD, dimmed (is_dimmed): DIMMED, + italic (is_italic): ITALIC, underline (is_underline): UNDERLINE, + blink (is_blink): BLINK, invert (is_invert): INVERT, + hidden (is_hidden): HIDDEN, + strikethrough (is_strikethrough): STRIKETHROUGH); + + #[inline(always)] + fn is_plain(&self) -> bool { + self == &Style::default() + } + + /// Writes the ANSI code prefix for the currently set styles. + /// + /// This method is intended to be used inside of [`fmt::Display`] and + /// [`fmt::Debug`] implementations for custom or specialized use-cases. Most + /// users should use [`Paint`] for all painting needs. + /// + /// This method writes the ANSI code prefix irrespective of whether painting + /// is currently enabled or disabled. To write the prefix only if painting + /// is enabled, condition a call to this method on [`Paint::is_enabled()`]. + /// + /// [`fmt::Display`]: fmt::Display + /// [`fmt::Debug`]: fmt::Debug + /// [`Paint`]: Paint + /// [`Paint::is_enabled()`]: Paint::is_enabled() + /// + /// # Example + /// + /// ```rust + /// use std::fmt; + /// use yansi::Style; + /// + /// struct CustomItem { + /// item: u32, + /// style: Style + /// } + /// + /// impl fmt::Display for CustomItem { + /// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + /// self.style.fmt_prefix(f)?; + /// write!(f, "number: {}", self.item)?; + /// self.style.fmt_suffix(f) + /// } + /// } + /// ``` + pub fn fmt_prefix(&self, f: &mut fmt::Write) -> fmt::Result { + // A user may just want a code-free string when no styles are applied. + if self.is_plain() { + return Ok(()); + } + + let mut splice = false; + write!(f, "\x1B[")?; + + for i in self.properties.iter() { + let k = if i >= 5 { i + 2 } else { i + 1 }; + write_spliced(&mut splice, f, k)?; + } + + if self.background != Color::Unset { + write_spliced(&mut splice, f, "4")?; + self.background.ascii_fmt(f)?; + } + + if self.foreground != Color::Unset { + write_spliced(&mut splice, f, "3")?; + self.foreground.ascii_fmt(f)?; + } + + // All the codes end with an `m`. + write!(f, "m") + } + + /// Writes the ANSI code suffix for the currently set styles. + /// + /// This method is intended to be used inside of [`fmt::Display`] and + /// [`fmt::Debug`] implementations for custom or specialized use-cases. Most + /// users should use [`Paint`] for all painting needs. + /// + /// This method writes the ANSI code suffix irrespective of whether painting + /// is currently enabled or disabled. To write the suffix only if painting + /// is enabled, condition a call to this method on [`Paint::is_enabled()`]. + /// + /// [`fmt::Display`]: fmt::Display + /// [`fmt::Debug`]: fmt::Debug + /// [`Paint`]: Paint + /// [`Paint::is_enabled()`]: Paint::is_enabled() + /// + /// # Example + /// + /// ```rust + /// use std::fmt; + /// use yansi::Style; + /// + /// struct CustomItem { + /// item: u32, + /// style: Style + /// } + /// + /// impl fmt::Display for CustomItem { + /// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + /// self.style.fmt_prefix(f)?; + /// write!(f, "number: {}", self.item)?; + /// self.style.fmt_suffix(f) + /// } + /// } + /// ``` + pub fn fmt_suffix(&self, f: &mut fmt::Write) -> fmt::Result { + if self.is_plain() { + return Ok(()); + } + + write!(f, "\x1B[0m") + } +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..f3f6781 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,182 @@ +extern crate serial_test; + +use self::serial_test::serial; + +use super::Color::*; +use super::{Paint, Style}; + +macro_rules! assert_renders { + ($($input:expr => $expected:expr,)*) => { + $( + let (input, expected) = ($input.to_string(), $expected.to_string()); + if input != expected { + panic!("expected {:?}, got {:?} from {:?} ({:?})", + expected, input, $input.inner(), $input.style()) + } + )* + }; +} + +macro_rules! assert_disabled_renders { + ($($input:expr => $expected:expr,)*) => { + $( + Paint::disable(); + let (actual, expected) = ($input.to_string(), $expected.to_string()); + Paint::enable(); + assert_eq!(actual, expected); + )* + }; +} + +#[test] +#[serial] +fn colors_enabled() { + assert_renders! { + Paint::new("text/plain") => "text/plain", + Paint::red("hi") => "\x1B[31mhi\x1B[0m", + Paint::black("hi") => "\x1B[30mhi\x1B[0m", + Paint::yellow("hi").bold() => "\x1B[1;33mhi\x1B[0m", + Paint::new("hi").fg(Yellow).bold() => "\x1B[1;33mhi\x1B[0m", + Paint::blue("hi").underline() => "\x1B[4;34mhi\x1B[0m", + Paint::green("hi").bold().underline() => "\x1B[1;4;32mhi\x1B[0m", + Paint::green("hi").underline().bold() => "\x1B[1;4;32mhi\x1B[0m", + Paint::magenta("hi").bg(White) => "\x1B[47;35mhi\x1B[0m", + Paint::red("hi").bg(Blue).fg(Yellow) => "\x1B[44;33mhi\x1B[0m", + Paint::cyan("hi").bg(Blue).fg(Yellow) => "\x1B[44;33mhi\x1B[0m", + Paint::cyan("hi").bold().bg(White) => "\x1B[1;47;36mhi\x1B[0m", + Paint::cyan("hi").underline().bg(White) => "\x1B[4;47;36mhi\x1B[0m", + Paint::cyan("hi").bold().underline().bg(White) => "\x1B[1;4;47;36mhi\x1B[0m", + Paint::cyan("hi").underline().bold().bg(White) => "\x1B[1;4;47;36mhi\x1B[0m", + Paint::fixed(100, "hi") => "\x1B[38;5;100mhi\x1B[0m", + Paint::fixed(100, "hi").bg(Magenta) => "\x1B[45;38;5;100mhi\x1B[0m", + Paint::fixed(100, "hi").bg(Fixed(200)) => "\x1B[48;5;200;38;5;100mhi\x1B[0m", + Paint::rgb(70, 130, 180, "hi") => "\x1B[38;2;70;130;180mhi\x1B[0m", + Paint::rgb(70, 130, 180, "hi").bg(Blue) => "\x1B[44;38;2;70;130;180mhi\x1B[0m", + Paint::blue("hi").bg(RGB(70, 130, 180)) => "\x1B[48;2;70;130;180;34mhi\x1B[0m", + Paint::rgb(70, 130, 180, "hi").bg(RGB(5,10,15)) => "\x1B[48;2;5;10;15;38;2;70;130;180mhi\x1B[0m", + Paint::new("hi").bold() => "\x1B[1mhi\x1B[0m", + Paint::new("hi").underline() => "\x1B[4mhi\x1B[0m", + Paint::new("hi").bold().underline() => "\x1B[1;4mhi\x1B[0m", + Paint::new("hi").dimmed() => "\x1B[2mhi\x1B[0m", + Paint::new("hi").italic() => "\x1B[3mhi\x1B[0m", + Paint::new("hi").blink() => "\x1B[5mhi\x1B[0m", + Paint::new("hi").invert() => "\x1B[7mhi\x1B[0m", + Paint::new("hi").hidden() => "\x1B[8mhi\x1B[0m", + Paint::new("hi").strikethrough() => "\x1B[9mhi\x1B[0m", + } +} + +#[test] +#[serial] +fn colors_disabled() { + assert_disabled_renders! { + Paint::new("text/plain") => "text/plain", + Paint::red("hi") => "hi", + Paint::black("hi") => "hi", + Paint::yellow("hi").bold() => "hi", + Paint::new("hi").fg(Yellow).bold() => "hi", + Paint::blue("hi").underline() => "hi", + Paint::green("hi").bold().underline() => "hi", + Paint::green("hi").underline().bold() => "hi", + Paint::magenta("hi").bg(White) => "hi", + Paint::red("hi").bg(Blue).fg(Yellow) => "hi", + Paint::cyan("hi").bg(Blue).fg(Yellow) => "hi", + Paint::cyan("hi").bold().bg(White) => "hi", + Paint::cyan("hi").underline().bg(White) => "hi", + Paint::cyan("hi").bold().underline().bg(White) => "hi", + Paint::cyan("hi").underline().bold().bg(White) => "hi", + Paint::fixed(100, "hi") => "hi", + Paint::fixed(100, "hi").bg(Magenta) => "hi", + Paint::fixed(100, "hi").bg(Fixed(200)) => "hi", + Paint::rgb(70, 130, 180, "hi") => "hi", + Paint::rgb(70, 130, 180, "hi").bg(Blue) => "hi", + Paint::blue("hi").bg(RGB(70, 130, 180)) => "hi", + Paint::blue("hi").bg(RGB(70, 130, 180)).wrap() => "hi", + Paint::rgb(70, 130, 180, "hi").bg(RGB(5,10,15)) => "hi", + Paint::new("hi").bold() => "hi", + Paint::new("hi").underline() => "hi", + Paint::new("hi").bold().underline() => "hi", + Paint::new("hi").dimmed() => "hi", + Paint::new("hi").italic() => "hi", + Paint::new("hi").blink() => "hi", + Paint::new("hi").invert() => "hi", + Paint::new("hi").hidden() => "hi", + Paint::new("hi").strikethrough() => "hi", + Paint::new("hi").strikethrough().wrap() => "hi", + } +} + +#[test] +#[serial] +fn masked_when_disabled() { + assert_disabled_renders! { + Paint::masked("text/plain") => "", + Paint::masked("text/plain").mask() => "", + Paint::new("text/plain").mask() => "", + Paint::new("text/plain").mask() => "", + Paint::red("hi").mask() => "", + Paint::black("hi").mask() => "", + Paint::yellow("hi").bold().mask() => "", + Paint::cyan("hi").bg(Blue).fg(Yellow).mask() => "", + Paint::cyan("hi").underline().bold().bg(White).mask() => "", + } +} + +#[test] +#[serial] +fn masked_when_enabled() { + assert_renders! { + Paint::masked("text/plain") => "text/plain", + Paint::masked("text/plain").mask() => "text/plain", + Paint::black("hi").mask() => "\x1B[30mhi\x1B[0m", + Paint::yellow("hi").bold().mask() => "\x1B[1;33mhi\x1B[0m", + Paint::new("hi").fg(Yellow).bold().mask() => "\x1B[1;33mhi\x1B[0m", + Paint::cyan("hi").underline().bg(White).mask() => "\x1B[4;47;36mhi\x1B[0m", + Paint::cyan("hi").bold().underline().bg(White).mask() => "\x1B[1;4;47;36mhi\x1B[0m", + Paint::rgb(70, 130, 180, "hi").mask() => "\x1B[38;2;70;130;180mhi\x1B[0m", + Paint::new("hi").underline().mask() => "\x1B[4mhi\x1B[0m", + Paint::new("hi").bold().underline().mask() => "\x1B[1;4mhi\x1B[0m", + Paint::new("hi").hidden().mask() => "\x1B[8mhi\x1B[0m", + } +} + +#[test] +#[serial] +fn wrapping() { + let inner = || format!("{} b {}", Paint::red("a"), Paint::green("c")); + let inner2 = || format!("0 {} 1", Paint::magenta(&inner()).wrap()); + assert_renders! { + Paint::new("text/plain").wrap() => "text/plain", + Paint::new(&inner()).wrap() => &inner(), + Paint::new(&inner()).wrap() => + "\u{1b}[31ma\u{1b}[0m b \u{1b}[32mc\u{1b}[0m", + Paint::new(&inner()).fg(Blue).wrap() => + "\u{1b}[34m\u{1b}[31ma\u{1b}[0m\u{1b}[34m b \ + \u{1b}[32mc\u{1b}[0m\u{1b}[34m\u{1b}[0m", + Paint::new(&inner2()).wrap() => &inner2(), + Paint::new(&inner2()).wrap() => + "0 \u{1b}[35m\u{1b}[31ma\u{1b}[0m\u{1b}[35m b \ + \u{1b}[32mc\u{1b}[0m\u{1b}[35m\u{1b}[0m 1", + Paint::new(&inner2()).fg(Blue).wrap() => + "\u{1b}[34m0 \u{1b}[35m\u{1b}[31ma\u{1b}[0m\u{1b}[34m\u{1b}[35m b \ + \u{1b}[32mc\u{1b}[0m\u{1b}[34m\u{1b}[35m\u{1b}[0m\u{1b}[34m 1\u{1b}[0m", + } +} + +#[test] +fn hash_eq() { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + fn hash(t: &T) -> u64 { + let mut s = DefaultHasher::new(); + t.hash(&mut s); + s.finish() + } + + let a = Style::default(); + let b = Style::default().mask(); + + assert_eq!(a, b); + assert_eq!(hash(&a), hash(&b)); +} diff --git a/src/windows.rs b/src/windows.rs new file mode 100644 index 0000000..324cfc3 --- /dev/null +++ b/src/windows.rs @@ -0,0 +1,69 @@ +#[cfg(windows)] +mod windows_console { + use std::os::raw::c_void; + + #[allow(non_camel_case_types)] type c_ulong = u32; + #[allow(non_camel_case_types)] type c_int = i32; + type DWORD = c_ulong; + type LPDWORD = *mut DWORD; + type HANDLE = *mut c_void; + type BOOL = c_int; + + const ENABLE_VIRTUAL_TERMINAL_PROCESSING: DWORD = 0x0004; + const STD_OUTPUT_HANDLE: DWORD = 0xFFFFFFF5; + const STD_ERROR_HANDLE: DWORD = 0xFFFFFFF4; + const INVALID_HANDLE_VALUE: HANDLE = -1isize as HANDLE; + const FALSE: BOOL = 0; + const TRUE: BOOL = 1; + + // This is the win32 console API, taken from the 'winapi' crate. + extern "system" { + fn GetStdHandle(nStdHandle: DWORD) -> HANDLE; + fn GetConsoleMode(hConsoleHandle: HANDLE, lpMode: LPDWORD) -> BOOL; + fn SetConsoleMode(hConsoleHandle: HANDLE, dwMode: DWORD) -> BOOL; + } + + unsafe fn get_handle(handle_num: DWORD) -> Result { + match GetStdHandle(handle_num) { + handle if handle == INVALID_HANDLE_VALUE => Err(()), + handle => Ok(handle) + } + } + + unsafe fn enable_vt(handle: HANDLE) -> Result<(), ()> { + let mut dw_mode: DWORD = 0; + if GetConsoleMode(handle, &mut dw_mode) == FALSE { + return Err(()); + } + + dw_mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + match SetConsoleMode(handle, dw_mode) { + result if result == TRUE => Ok(()), + _ => Err(()) + } + } + + unsafe fn enable_ascii_colors_raw() -> Result { + let stdout_handle = get_handle(STD_OUTPUT_HANDLE)?; + let stderr_handle = get_handle(STD_ERROR_HANDLE)?; + + enable_vt(stdout_handle)?; + if stdout_handle != stderr_handle { + enable_vt(stderr_handle)?; + } + + Ok(true) + } + + #[inline] + pub fn enable_ascii_colors() -> bool { + unsafe { enable_ascii_colors_raw().unwrap_or(false) } + } +} + +#[cfg(not(windows))] +mod windows_console { + pub fn enable_ascii_colors() -> bool { true } +} + +pub use self::windows_console::enable_ascii_colors; -- 2.7.4