From 1eae77b4c69645005eab9f368cabb4250d261b80 Mon Sep 17 00:00:00 2001 From: DongHun Kwak Date: Mon, 27 Mar 2023 14:00:52 +0900 Subject: [PATCH] Import anstyle 0.3.5 --- .cargo_vcs_info.json | 6 + Cargo.lock | 16 ++ Cargo.toml | 84 +++++++ Cargo.toml.orig | 31 +++ LICENSE-APACHE | 202 +++++++++++++++ LICENSE-MIT | 19 ++ README.md | 42 ++++ examples/dump.rs | 137 +++++++++++ src/color.rs | 678 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/effect.rs | 368 ++++++++++++++++++++++++++++ src/lib.rs | 59 +++++ src/macros.rs | 5 + src/reset.rs | 21 ++ src/style.rs | 469 +++++++++++++++++++++++++++++++++++ 14 files changed, 2137 insertions(+) create mode 100644 .cargo_vcs_info.json create mode 100644 Cargo.lock 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 examples/dump.rs create mode 100644 src/color.rs create mode 100644 src/effect.rs create mode 100644 src/lib.rs create mode 100644 src/macros.rs create mode 100644 src/reset.rs create mode 100644 src/style.rs diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..cbd97d9 --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "9d7cd0335e13fbe5d032458d7deca90bda188c27" + }, + "path_in_vcs": "crates/anstyle" +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1454282 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstyle" +version = "0.3.5" +dependencies = [ + "lexopt", +] + +[[package]] +name = "lexopt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baff4b617f7df3d896f97fe922b64817f6cd9a756bb81d40f8883f2f66dcb401" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..70e550e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,84 @@ +# 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] +edition = "2021" +rust-version = "1.64.0" +name = "anstyle" +version = "0.3.5" +include = [ + "build.rs", + "src/**/*", + "Cargo.toml", + "LICENSE*", + "README.md", + "benches/**/*", + "examples/**/*", +] +description = "ANSI text styling" +homepage = "https://github.com/rust-cli/anstyle" +readme = "README.md" +keywords = [ + "ansi", + "terminal", + "color", + "no_std", +] +categories = ["command-line-interface"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-cli/anstyle.git" + +[package.metadata.release] +tag-prefix = "" + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "Unreleased" +replace = "{{version}}" +min = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = '\.\.\.HEAD' +replace = "...{{tag_name}}" +exactly = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "ReleaseDate" +replace = "{{date}}" +min = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "" +replace = """ + +## [Unreleased] - ReleaseDate +""" +exactly = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "" +replace = """ + +[Unreleased]: https://github.com/rust-cli/anstyle/compare/{{tag_name}}...HEAD""" +exactly = 1 + +[dependencies] + +[dev-dependencies.lexopt] +version = "0.3.0" + +[features] +default = ["std"] +std = [] diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..a6e5ac0 --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,31 @@ +[package] +name = "anstyle" +version = "0.3.5" +description = "ANSI text styling" +repository = "https://github.com/rust-cli/anstyle.git" +homepage = "https://github.com/rust-cli/anstyle" +categories = ["command-line-interface"] +keywords = ["ansi", "terminal", "color", "no_std"] +license.workspace = true +edition.workspace = true +rust-version.workspace = true +include.workspace = true + +[package.metadata.release] +tag-prefix = "" +pre-release-replacements = [ + {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, + {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, + {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, + {file="CHANGELOG.md", search="", replace="\n## [Unreleased] - ReleaseDate\n", exactly=1}, + {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/rust-cli/anstyle/compare/{{tag_name}}...HEAD", exactly=1}, +] + +[features] +default = ["std"] +std = [] + +[dependencies] + +[dev-dependencies] +lexopt = "0.3.0" diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..8f71f43 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,202 @@ + 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..9dc20e2 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) 2022 The rust-cli Developers + +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..706e8a1 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# anstyle + +> ANSI text styling + +*A portmanteau of "ansi style"* + +[![Documentation](https://img.shields.io/badge/docs-master-blue.svg)][Documentation] +![License](https://img.shields.io/crates/l/anstyle.svg) +[![Crates Status](https://img.shields.io/crates/v/anstyle.svg)](https://crates.io/crates/anstyle) + +`anstyle` provides core types describing [ANSI styling escape +codes](https://en.wikipedia.org/wiki/ANSI_escape_code) for interoperability +between crates. For example, this would allow a crate to provide an API for +customizing the colors used without putting the underlying text styling crate +in the API. + +For integration with your text styling crate, see: +- [anstyle-termcolor](crates/termcolor) +- [anstyle-owo-colors](crates/owo) +- [anstyle-yansi](crates/yansi) + +General utilities: +- [anstyle-git](crates/git): Parse Git style descriptions + +## License + +Licensed under either of + + * 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) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. + +[Crates.io]: https://crates.io/crates/anstyle +[Documentation]: https://docs.rs/anstyle diff --git a/examples/dump.rs b/examples/dump.rs new file mode 100644 index 0000000..a37a048 --- /dev/null +++ b/examples/dump.rs @@ -0,0 +1,137 @@ +use std::io::Write; + +fn main() -> Result<(), lexopt::Error> { + let args = Args::parse()?; + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + + for fixed in 0..16 { + let style = style(fixed, args.layer, args.effects); + let _ = print_number(&mut stdout, fixed, style); + if fixed == 7 || fixed == 15 { + let _ = writeln!(&mut stdout); + } + } + + for r in 0..6 { + let _ = writeln!(stdout); + for g in 0..6 { + for b in 0..6 { + let fixed = r * 36 + g * 6 + b + 16; + let style = style(fixed, args.layer, args.effects); + let _ = print_number(&mut stdout, fixed, style); + } + let _ = writeln!(stdout); + } + } + + for c in 0..24 { + if 0 == c % 8 { + let _ = writeln!(stdout); + } + let fixed = 232 + c; + let style = style(fixed, args.layer, args.effects); + let _ = print_number(&mut stdout, fixed, style); + } + + Ok(()) +} + +fn style(fixed: u8, layer: Layer, effects: anstyle::Effects) -> anstyle::Style { + let color = anstyle::Ansi256Color(fixed).into(); + (match layer { + Layer::Fg => anstyle::Style::new().fg_color(Some(color)), + Layer::Bg => anstyle::Style::new().bg_color(Some(color)), + Layer::Underline => anstyle::Style::new().underline_color(Some(color)), + }) | effects +} + +fn print_number( + stdout: &mut std::io::StdoutLock<'_>, + fixed: u8, + style: anstyle::Style, +) -> std::io::Result<()> { + write!( + stdout, + "{}{:>4}{}", + style.render(), + fixed, + anstyle::Reset.render() + ) +} + +#[derive(Default)] +struct Args { + effects: anstyle::Effects, + layer: Layer, +} + +#[derive(Copy, Clone)] +enum Layer { + Fg, + Bg, + Underline, +} + +impl Default for Layer { + fn default() -> Self { + Layer::Fg + } +} + +impl Args { + fn parse() -> Result { + use lexopt::prelude::*; + + let mut res = Args::default(); + + let mut args = lexopt::Parser::from_env(); + while let Some(arg) = args.next()? { + match arg { + Long("layer") => { + res.layer = args.value()?.parse_with(|s| match s { + "fg" => Ok(Layer::Fg), + "bg" => Ok(Layer::Bg), + "underline" => Ok(Layer::Underline), + _ => Err("expected values fg, bg, underline"), + })?; + } + Long("effect") => { + const EFFECTS: [(&str, anstyle::Effects); 12] = [ + ("bold", anstyle::Effects::BOLD), + ("dimmed", anstyle::Effects::DIMMED), + ("italic", anstyle::Effects::ITALIC), + ("underline", anstyle::Effects::UNDERLINE), + ("double_underline", anstyle::Effects::DOUBLE_UNDERLINE), + ("curly_underline", anstyle::Effects::CURLY_UNDERLINE), + ("dotted_underline", anstyle::Effects::DOTTED_UNDERLINE), + ("dashed_underline", anstyle::Effects::DASHED_UNDERLINE), + ("blink", anstyle::Effects::BLINK), + ("invert", anstyle::Effects::INVERT), + ("hidden", anstyle::Effects::HIDDEN), + ("strikethrough", anstyle::Effects::STRIKETHROUGH), + ]; + let effect = args.value()?.parse_with(|s| { + EFFECTS + .into_iter() + .find(|(name, _)| *name == s) + .map(|(_, effect)| effect) + .ok_or_else(|| { + format!( + "expected one of {}", + EFFECTS + .into_iter() + .map(|(n, _)| n) + .collect::>() + .join(", ") + ) + }) + })?; + res.effects = res.effects.insert(effect); + } + _ => return Err(arg.unexpected()), + } + } + Ok(res) + } +} diff --git a/src/color.rs b/src/color.rs new file mode 100644 index 0000000..b56d13e --- /dev/null +++ b/src/color.rs @@ -0,0 +1,678 @@ +/// Any ANSI color code scheme +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Color { + Ansi(AnsiColor), + Ansi256(Ansi256Color), + Rgb(RgbColor), +} + +impl Color { + /// Create a [`Style`][crate::Style] with this as the foregroun + #[inline] + pub fn on(self, background: impl Into) -> crate::Style { + crate::Style::new() + .fg_color(Some(self)) + .bg_color(Some(background.into())) + } + + /// Create a [`Style`][crate::Style] with this as the foregroun + #[inline] + pub fn on_default(self) -> crate::Style { + crate::Style::new().fg_color(Some(self)) + } + + /// Render the ANSI code for a foreground color + #[inline] + pub fn render_fg(self) -> impl core::fmt::Display { + match self { + Self::Ansi(color) => DisplayBuffer::default().write_str(color.as_fg_str()), + Self::Ansi256(color) => color.as_fg_buffer(), + Self::Rgb(color) => color.as_fg_buffer(), + } + } + + #[inline] + #[cfg(feature = "std")] + pub(crate) fn write_fg_to(self, write: &mut dyn std::io::Write) -> std::io::Result<()> { + let buffer = match self { + Self::Ansi(color) => DisplayBuffer::default().write_str(color.as_fg_str()), + Self::Ansi256(color) => color.as_fg_buffer(), + Self::Rgb(color) => color.as_fg_buffer(), + }; + buffer.write_to(write) + } + + /// Render the ANSI code for a background color + #[inline] + pub fn render_bg(self) -> impl core::fmt::Display { + match self { + Self::Ansi(color) => DisplayBuffer::default().write_str(color.as_bg_str()), + Self::Ansi256(color) => color.as_bg_buffer(), + Self::Rgb(color) => color.as_bg_buffer(), + } + } + + #[inline] + #[cfg(feature = "std")] + pub(crate) fn write_bg_to(self, write: &mut dyn std::io::Write) -> std::io::Result<()> { + let buffer = match self { + Self::Ansi(color) => DisplayBuffer::default().write_str(color.as_bg_str()), + Self::Ansi256(color) => color.as_bg_buffer(), + Self::Rgb(color) => color.as_bg_buffer(), + }; + buffer.write_to(write) + } + + #[inline] + pub(crate) fn render_underline(self) -> impl core::fmt::Display { + match self { + Self::Ansi(color) => color.as_underline_buffer(), + Self::Ansi256(color) => color.as_underline_buffer(), + Self::Rgb(color) => color.as_underline_buffer(), + } + } + + #[inline] + #[cfg(feature = "std")] + pub(crate) fn write_underline_to(self, write: &mut dyn std::io::Write) -> std::io::Result<()> { + let buffer = match self { + Self::Ansi(color) => color.as_underline_buffer(), + Self::Ansi256(color) => color.as_underline_buffer(), + Self::Rgb(color) => color.as_underline_buffer(), + }; + buffer.write_to(write) + } +} + +impl From for Color { + #[inline] + fn from(inner: AnsiColor) -> Self { + Self::Ansi(inner) + } +} + +impl From for Color { + #[inline] + fn from(inner: Ansi256Color) -> Self { + Self::Ansi256(inner) + } +} + +impl From for Color { + #[inline] + fn from(inner: RgbColor) -> Self { + Self::Rgb(inner) + } +} + +impl From for Color { + #[inline] + fn from(inner: u8) -> Self { + Self::Ansi256(inner.into()) + } +} + +impl From<(u8, u8, u8)> for Color { + #[inline] + fn from(inner: (u8, u8, u8)) -> Self { + Self::Rgb(inner.into()) + } +} + +/// Define style with specified foreground color and effects +/// +/// # Examples +/// +/// ```rust +/// let fg = anstyle::Color::from((0, 0, 0)); +/// let style = fg | anstyle::Effects::BOLD | anstyle::Effects::UNDERLINE; +/// ``` +impl core::ops::BitOr for Color { + type Output = crate::Style; + + #[inline(always)] + fn bitor(self, rhs: crate::Effects) -> Self::Output { + crate::Style::new().fg_color(Some(self)) | rhs + } +} + +/// Available 4-bit ANSI color palette codes +/// +/// The user's terminal defines the meaning of the each palette code. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] +pub enum AnsiColor { + /// 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, + + /// Bright black: #0 (foreground code `90`, background code `100`). + BrightBlack, + + /// Bright red: #1 (foreground code `91`, background code `101`). + BrightRed, + + /// Bright green: #2 (foreground code `92`, background code `102`). + BrightGreen, + + /// Bright yellow: #3 (foreground code `93`, background code `103`). + BrightYellow, + + /// Bright blue: #4 (foreground code `94`, background code `104`). + BrightBlue, + + /// Bright magenta: #5 (foreground code `95`, background code `105`). + BrightMagenta, + + /// Bright cyan: #6 (foreground code `96`, background code `106`). + BrightCyan, + + /// Bright white: #7 (foreground code `97`, background code `107`). + BrightWhite, +} + +impl AnsiColor { + /// Create a [`Style`][crate::Style] with this as the foregroun + #[inline] + pub fn on(self, background: impl Into) -> crate::Style { + crate::Style::new() + .fg_color(Some(self.into())) + .bg_color(Some(background.into())) + } + + /// Create a [`Style`][crate::Style] with this as the foregroun + #[inline] + pub fn on_default(self) -> crate::Style { + crate::Style::new().fg_color(Some(self.into())) + } + + /// Render the ANSI code for a foreground color + #[inline] + pub fn render_fg(self) -> impl core::fmt::Display { + self.as_fg_str() + } + + #[inline] + fn as_fg_str(&self) -> &'static str { + match self { + Self::Black => escape!("3", "0"), + Self::Red => escape!("3", "1"), + Self::Green => escape!("3", "2"), + Self::Yellow => escape!("3", "3"), + Self::Blue => escape!("3", "4"), + Self::Magenta => escape!("3", "5"), + Self::Cyan => escape!("3", "6"), + Self::White => escape!("3", "7"), + Self::BrightBlack => escape!("9", "0"), + Self::BrightRed => escape!("9", "1"), + Self::BrightGreen => escape!("9", "2"), + Self::BrightYellow => escape!("9", "3"), + Self::BrightBlue => escape!("9", "4"), + Self::BrightMagenta => escape!("9", "5"), + Self::BrightCyan => escape!("9", "6"), + Self::BrightWhite => escape!("9", "7"), + } + } + + /// Render the ANSI code for a background color + #[inline] + pub fn render_bg(self) -> impl core::fmt::Display { + self.as_bg_str() + } + + #[inline] + fn as_bg_str(&self) -> &'static str { + match self { + Self::Black => escape!("4", "0"), + Self::Red => escape!("4", "1"), + Self::Green => escape!("4", "2"), + Self::Yellow => escape!("4", "3"), + Self::Blue => escape!("4", "4"), + Self::Magenta => escape!("4", "5"), + Self::Cyan => escape!("4", "6"), + Self::White => escape!("4", "7"), + Self::BrightBlack => escape!("10", "0"), + Self::BrightRed => escape!("10", "1"), + Self::BrightGreen => escape!("10", "2"), + Self::BrightYellow => escape!("10", "3"), + Self::BrightBlue => escape!("10", "4"), + Self::BrightMagenta => escape!("10", "5"), + Self::BrightCyan => escape!("10", "6"), + Self::BrightWhite => escape!("10", "7"), + } + } + + #[inline] + fn as_underline_buffer(&self) -> DisplayBuffer { + // No per-color codes; must delegate to `Ansi256Color` + Ansi256Color::from(*self).as_underline_buffer() + } + + /// Change the color to/from bright + #[must_use] + #[inline] + pub fn bright(self, yes: bool) -> Self { + if yes { + match self { + Self::Black => Self::BrightBlack, + Self::Red => Self::BrightRed, + Self::Green => Self::BrightGreen, + Self::Yellow => Self::BrightYellow, + Self::Blue => Self::BrightBlue, + Self::Magenta => Self::BrightMagenta, + Self::Cyan => Self::BrightCyan, + Self::White => Self::BrightWhite, + Self::BrightBlack => self, + Self::BrightRed => self, + Self::BrightGreen => self, + Self::BrightYellow => self, + Self::BrightBlue => self, + Self::BrightMagenta => self, + Self::BrightCyan => self, + Self::BrightWhite => self, + } + } else { + match self { + Self::Black => self, + Self::Red => self, + Self::Green => self, + Self::Yellow => self, + Self::Blue => self, + Self::Magenta => self, + Self::Cyan => self, + Self::White => self, + Self::BrightBlack => Self::Black, + Self::BrightRed => Self::Red, + Self::BrightGreen => Self::Green, + Self::BrightYellow => Self::Yellow, + Self::BrightBlue => Self::Blue, + Self::BrightMagenta => Self::Magenta, + Self::BrightCyan => Self::Cyan, + Self::BrightWhite => Self::White, + } + } + } + + /// Report whether the color is bright + #[inline] + pub fn is_bright(self) -> bool { + match self { + Self::Black => false, + Self::Red => false, + Self::Green => false, + Self::Yellow => false, + Self::Blue => false, + Self::Magenta => false, + Self::Cyan => false, + Self::White => false, + Self::BrightBlack => true, + Self::BrightRed => true, + Self::BrightGreen => true, + Self::BrightYellow => true, + Self::BrightBlue => true, + Self::BrightMagenta => true, + Self::BrightCyan => true, + Self::BrightWhite => true, + } + } +} + +/// Define style with specified foreground color and effects +/// +/// # Examples +/// +/// ```rust +/// let fg = anstyle::AnsiColor::Black; +/// let style = fg | anstyle::Effects::BOLD | anstyle::Effects::UNDERLINE; +/// ``` +impl core::ops::BitOr for AnsiColor { + type Output = crate::Style; + + #[inline(always)] + fn bitor(self, rhs: crate::Effects) -> Self::Output { + crate::Style::new().fg_color(Some(self.into())) | rhs + } +} + +/// 256 (8-bit) color support +/// +/// - `0..16` are [`AnsiColor`] palette codes +/// - `0..232` map to [`RgbColor`] color values +/// - `232..` map to [`RgbColor`] gray-scale values +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct Ansi256Color(pub u8); + +impl Ansi256Color { + /// Create a [`Style`][crate::Style] with this as the foregroun + #[inline] + pub fn on(self, background: impl Into) -> crate::Style { + crate::Style::new() + .fg_color(Some(self.into())) + .bg_color(Some(background.into())) + } + + /// Create a [`Style`][crate::Style] with this as the foregroun + #[inline] + pub fn on_default(self) -> crate::Style { + crate::Style::new().fg_color(Some(self.into())) + } + + #[inline] + pub const fn index(self) -> u8 { + self.0 + } + + #[inline] + pub const fn into_ansi(self) -> Option { + match self.index() { + 0 => Some(AnsiColor::Black), + 1 => Some(AnsiColor::Red), + 2 => Some(AnsiColor::Green), + 3 => Some(AnsiColor::Yellow), + 4 => Some(AnsiColor::Blue), + 5 => Some(AnsiColor::Magenta), + 6 => Some(AnsiColor::Cyan), + 7 => Some(AnsiColor::White), + 8 => Some(AnsiColor::BrightBlack), + 9 => Some(AnsiColor::BrightRed), + 10 => Some(AnsiColor::BrightGreen), + 11 => Some(AnsiColor::BrightYellow), + 12 => Some(AnsiColor::BrightBlue), + 13 => Some(AnsiColor::BrightMagenta), + 14 => Some(AnsiColor::BrightCyan), + 15 => Some(AnsiColor::BrightWhite), + _ => None, + } + } + + #[inline] + pub const fn from_ansi(color: AnsiColor) -> Self { + match color { + AnsiColor::Black => Self(0), + AnsiColor::Red => Self(1), + AnsiColor::Green => Self(2), + AnsiColor::Yellow => Self(3), + AnsiColor::Blue => Self(4), + AnsiColor::Magenta => Self(5), + AnsiColor::Cyan => Self(6), + AnsiColor::White => Self(7), + AnsiColor::BrightBlack => Self(8), + AnsiColor::BrightRed => Self(9), + AnsiColor::BrightGreen => Self(10), + AnsiColor::BrightYellow => Self(11), + AnsiColor::BrightBlue => Self(12), + AnsiColor::BrightMagenta => Self(13), + AnsiColor::BrightCyan => Self(14), + AnsiColor::BrightWhite => Self(15), + } + } + + /// Render the ANSI code for a foreground color + #[inline] + pub fn render_fg(self) -> impl core::fmt::Display { + self.as_fg_buffer() + } + + #[inline] + fn as_fg_buffer(&self) -> DisplayBuffer { + DisplayBuffer::default() + .write_str("\x1B[38;5;") + .write_code(self.index()) + .write_str("m") + } + + /// Render the ANSI code for a background color + #[inline] + pub fn render_bg(self) -> impl core::fmt::Display { + self.as_bg_buffer() + } + + #[inline] + fn as_bg_buffer(&self) -> DisplayBuffer { + DisplayBuffer::default() + .write_str("\x1B[48;5;") + .write_code(self.index()) + .write_str("m") + } + + #[inline] + fn as_underline_buffer(&self) -> DisplayBuffer { + DisplayBuffer::default() + .write_str("\x1B[58;5;") + .write_code(self.index()) + .write_str("m") + } +} + +impl From for Ansi256Color { + #[inline] + fn from(inner: u8) -> Self { + Self(inner) + } +} + +impl From for Ansi256Color { + #[inline] + fn from(inner: AnsiColor) -> Self { + Self::from_ansi(inner) + } +} + +/// Define style with specified foreground color and effects +/// +/// # Examples +/// +/// ```rust +/// let fg = anstyle::Ansi256Color(0); +/// let style = fg | anstyle::Effects::BOLD | anstyle::Effects::UNDERLINE; +/// ``` +impl core::ops::BitOr for Ansi256Color { + type Output = crate::Style; + + #[inline(always)] + fn bitor(self, rhs: crate::Effects) -> Self::Output { + crate::Style::new().fg_color(Some(self.into())) | rhs + } +} + +/// 24-bit ANSI RGB color codes +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RgbColor(pub u8, pub u8, pub u8); + +impl RgbColor { + /// Create a [`Style`][crate::Style] with this as the foregroun + #[inline] + pub fn on(self, background: impl Into) -> crate::Style { + crate::Style::new() + .fg_color(Some(self.into())) + .bg_color(Some(background.into())) + } + + /// Create a [`Style`][crate::Style] with this as the foregroun + #[inline] + pub fn on_default(self) -> crate::Style { + crate::Style::new().fg_color(Some(self.into())) + } + + #[inline] + pub const fn r(self) -> u8 { + self.0 + } + + #[inline] + pub const fn g(self) -> u8 { + self.1 + } + + #[inline] + pub const fn b(self) -> u8 { + self.2 + } + + /// Render the ANSI code for a foreground color + #[inline] + pub fn render_fg(self) -> impl core::fmt::Display { + self.as_fg_buffer() + } + + #[inline] + fn as_fg_buffer(&self) -> DisplayBuffer { + DisplayBuffer::default() + .write_str("\x1B[38;2;") + .write_code(self.r()) + .write_str(";") + .write_code(self.g()) + .write_str(";") + .write_code(self.b()) + .write_str("m") + } + + /// Render the ANSI code for a background color + #[inline] + pub fn render_bg(self) -> impl core::fmt::Display { + self.as_bg_buffer() + } + + #[inline] + fn as_bg_buffer(&self) -> DisplayBuffer { + DisplayBuffer::default() + .write_str("\x1B[48;2;") + .write_code(self.r()) + .write_str(";") + .write_code(self.g()) + .write_str(";") + .write_code(self.b()) + .write_str("m") + } + + #[inline] + fn as_underline_buffer(&self) -> DisplayBuffer { + DisplayBuffer::default() + .write_str("\x1B[58;2;") + .write_code(self.r()) + .write_str(";") + .write_code(self.g()) + .write_str(";") + .write_code(self.b()) + .write_str("m") + } +} + +impl From<(u8, u8, u8)> for RgbColor { + #[inline] + fn from(inner: (u8, u8, u8)) -> Self { + let (r, g, b) = inner; + Self(r, g, b) + } +} + +/// Define style with specified foreground color and effects +/// +/// # Examples +/// +/// ```rust +/// let fg = anstyle::RgbColor(0, 0, 0); +/// let style = fg | anstyle::Effects::BOLD | anstyle::Effects::UNDERLINE; +/// ``` +impl core::ops::BitOr for RgbColor { + type Output = crate::Style; + + #[inline(always)] + fn bitor(self, rhs: crate::Effects) -> Self::Output { + crate::Style::new().fg_color(Some(self.into())) | rhs + } +} + +#[derive(Default, Debug)] +struct DisplayBuffer { + buffer: [u8; 19], + len: usize, +} + +impl DisplayBuffer { + #[must_use] + #[inline(never)] + fn write_str(mut self, part: &'static str) -> Self { + for (i, b) in part.as_bytes().iter().enumerate() { + self.buffer[self.len + i] = *b; + } + self.len += part.len(); + self + } + + #[must_use] + #[inline(never)] + fn write_code(mut self, code: u8) -> Self { + let c1: u8 = (code / 100) % 10; + let c2: u8 = (code / 10) % 10; + let c3: u8 = code % 10; + + let mut printed = true; + if c1 != 0 { + printed = true; + self.buffer[self.len] = b'0' + c1; + self.len += 1; + } + if c2 != 0 || printed { + self.buffer[self.len] = b'0' + c2; + self.len += 1; + } + // If we received a zero value we must still print a value. + self.buffer[self.len] = b'0' + c3; + self.len += 1; + + self + } + + #[inline] + fn as_str(&self) -> &str { + // SAFETY: Only `&str` can be written to the buffer + unsafe { core::str::from_utf8_unchecked(&self.buffer[0..self.len]) } + } + + #[inline] + #[cfg(feature = "std")] + fn write_to(self, write: &mut dyn std::io::Write) -> std::io::Result<()> { + write.write_all(self.as_str().as_bytes()) + } +} + +impl core::fmt::Display for DisplayBuffer { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.as_str().fmt(f) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn max_display_buffer() { + let c = RgbColor(255, 255, 255); + let actual = c.render_fg().to_string(); + assert_eq!(actual, "\u{1b}[38;2;255;255;255m"); + } +} diff --git a/src/effect.rs b/src/effect.rs new file mode 100644 index 0000000..d31dafe --- /dev/null +++ b/src/effect.rs @@ -0,0 +1,368 @@ +/// A set of text effects +/// +/// # Examples +/// +/// ```rust +/// let effects = anstyle::Effects::BOLD | anstyle::Effects::UNDERLINE; +/// ``` +#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Effects(u16); + +impl Effects { + const PLAIN: Self = Effects(0); + + pub const BOLD: Self = Effects(1 << 0); + pub const DIMMED: Self = Effects(1 << 1); + /// Not widely supported. Sometimes treated as inverse or blink + pub const ITALIC: Self = Effects(1 << 2); + /// Style extensions exist for Kitty, VTE, mintty and iTerm2. + pub const UNDERLINE: Self = Effects(1 << 3); + pub const DOUBLE_UNDERLINE: Self = Effects(1 << 4); + pub const CURLY_UNDERLINE: Self = Effects(1 << 5); + pub const DOTTED_UNDERLINE: Self = Effects(1 << 6); + pub const DASHED_UNDERLINE: Self = Effects(1 << 7); + pub const BLINK: Self = Effects(1 << 8); + /// Swap foreground and background colors; inconsistent emulation + pub const INVERT: Self = Effects(1 << 9); + pub const HIDDEN: Self = Effects(1 << 10); + /// Characters legible but marked as if for deletion. Not supported in Terminal.app + pub const STRIKETHROUGH: Self = Effects(1 << 11); + + /// No effects enabled + /// + /// # Examples + /// + /// ```rust + /// let effects = anstyle::Effects::new(); + /// ``` + #[inline] + pub const fn new() -> Self { + Self::PLAIN + } + + /// Check if no effects are enabled + /// + /// # Examples + /// + /// ```rust + /// let effects = anstyle::Effects::new(); + /// assert!(effects.is_plain()); + /// + /// let effects = anstyle::Effects::BOLD | anstyle::Effects::UNDERLINE; + /// assert!(!effects.is_plain()); + /// ``` + #[inline] + pub const fn is_plain(self) -> bool { + self.0 == Self::PLAIN.0 + } + + /// Returns `true` if all of the effects in `other` are contained within `self`. + /// + /// # Examples + /// + /// ```rust + /// let effects = anstyle::Effects::BOLD | anstyle::Effects::UNDERLINE; + /// assert!(effects.contains(anstyle::Effects::BOLD)); + /// + /// let effects = anstyle::Effects::new(); + /// assert!(!effects.contains(anstyle::Effects::BOLD)); + /// ``` + #[inline(always)] + pub const fn contains(self, other: Effects) -> bool { + (other.0 & self.0) == other.0 + } + + /// Inserts the specified effects in-place. + /// + /// # Examples + /// + /// ```rust + /// let effects = anstyle::Effects::new().insert(anstyle::Effects::new()); + /// assert!(effects.is_plain()); + /// + /// let effects = anstyle::Effects::new().insert(anstyle::Effects::BOLD); + /// assert!(effects.contains(anstyle::Effects::BOLD)); + /// ``` + #[inline(always)] + #[must_use] + pub const fn insert(mut self, other: Effects) -> Self { + self.0 |= other.0; + self + } + + /// Removes the specified effects in-place. + /// + /// # Examples + /// + /// ```rust + /// let effects = (anstyle::Effects::BOLD | anstyle::Effects::UNDERLINE).remove(anstyle::Effects::BOLD); + /// assert!(!effects.contains(anstyle::Effects::BOLD)); + /// assert!(effects.contains(anstyle::Effects::UNDERLINE)); + /// ``` + #[inline(always)] + #[must_use] + pub const fn remove(mut self, other: Effects) -> Self { + self.0 &= !other.0; + self + } + + /// Reset all effects in-place + /// ```rust + /// let effects = (anstyle::Effects::BOLD | anstyle::Effects::UNDERLINE).clear(); + /// assert!(!effects.contains(anstyle::Effects::BOLD)); + /// assert!(!effects.contains(anstyle::Effects::UNDERLINE)); + /// ``` + #[inline(always)] + #[must_use] + pub const fn clear(self) -> Self { + Self::new() + } + + /// Enable or disable the specified effects depending on the passed value. + /// + /// # Examples + /// + /// ```rust + /// let effects = anstyle::Effects::new().set(anstyle::Effects::BOLD, true); + /// assert!(effects.contains(anstyle::Effects::BOLD)); + /// ``` + #[inline] + #[must_use] + pub const fn set(self, other: Self, enable: bool) -> Self { + if enable { + self.insert(other) + } else { + self.remove(other) + } + } + + /// Iterate over enabled effects + #[inline(always)] + pub fn iter(self) -> EffectIter { + EffectIter { + index: 0, + effects: self, + } + } + + /// Iterate over enabled effect indices + #[inline(always)] + pub(crate) fn index_iter(self) -> EffectIndexIter { + EffectIndexIter { + index: 0, + effects: self, + } + } + + /// Render the ANSI code + #[inline] + pub fn render(self) -> impl core::fmt::Display { + EffectsDisplay(self) + } + + #[inline] + #[cfg(feature = "std")] + pub(crate) fn write_to(self, write: &mut dyn std::io::Write) -> std::io::Result<()> { + for index in self.index_iter() { + write.write_all(METADATA[index].escape.as_bytes())?; + } + Ok(()) + } +} + +/// # Examples +/// +/// ```rust +/// let effects = anstyle::Effects::new(); +/// assert_eq!(format!("{:?}", effects), "Effects()"); +/// +/// let effects = anstyle::Effects::BOLD | anstyle::Effects::UNDERLINE; +/// assert_eq!(format!("{:?}", effects), "Effects(BOLD | UNDERLINE)"); +/// ``` +impl core::fmt::Debug for Effects { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Effects(")?; + for (i, index) in self.index_iter().enumerate() { + if i != 0 { + write!(f, " | ")?; + } + write!(f, "{}", METADATA[index].name)?; + } + write!(f, ")")?; + Ok(()) + } +} + +/// # Examples +/// +/// ```rust +/// let effects = anstyle::Effects::BOLD | anstyle::Effects::UNDERLINE; +/// assert_eq!(format!("{:?}", effects), "Effects(BOLD | UNDERLINE)"); +/// ``` +impl core::ops::BitOr for Effects { + type Output = Self; + + #[inline(always)] + fn bitor(self, rhs: Self) -> Self { + self.insert(rhs) + } +} + +/// # Examples +/// +/// ```rust +/// let mut effects = anstyle::Effects::BOLD; +/// effects |= anstyle::Effects::UNDERLINE; +/// assert_eq!(format!("{:?}", effects), "Effects(BOLD | UNDERLINE)"); +/// ``` +impl core::ops::BitOrAssign for Effects { + #[inline] + fn bitor_assign(&mut self, other: Self) { + *self = self.insert(other); + } +} + +/// # Examples +/// +/// ```rust +/// let effects = (anstyle::Effects::BOLD | anstyle::Effects::UNDERLINE) - anstyle::Effects::BOLD; +/// assert_eq!(format!("{:?}", effects), "Effects(UNDERLINE)"); +/// ``` +impl core::ops::Sub for Effects { + type Output = Self; + + #[inline] + fn sub(self, other: Self) -> Self { + self.remove(other) + } +} + +/// # Examples +/// +/// ```rust +/// let mut effects = anstyle::Effects::BOLD | anstyle::Effects::UNDERLINE; +/// effects -= anstyle::Effects::BOLD; +/// assert_eq!(format!("{:?}", effects), "Effects(UNDERLINE)"); +/// ``` +impl core::ops::SubAssign for Effects { + #[inline] + fn sub_assign(&mut self, other: Self) { + *self = self.remove(other); + } +} + +pub(crate) struct Metadata { + pub(crate) name: &'static str, + pub(crate) escape: &'static str, +} + +pub(crate) const METADATA: [Metadata; 12] = [ + Metadata { + name: "BOLD", + escape: escape!("1"), + }, + Metadata { + name: "DIMMED", + escape: escape!("2"), + }, + Metadata { + name: "ITALIC", + escape: escape!("3"), + }, + Metadata { + name: "UNDERLINE", + escape: escape!("4"), + }, + Metadata { + name: "DOUBLE_UNDERLINE", + escape: escape!("21"), + }, + Metadata { + name: "CURLY_UNDERLINE", + escape: escape!("4:3"), + }, + Metadata { + name: "DOTTED_UNDERLINE", + escape: escape!("4:4"), + }, + Metadata { + name: "DASHED_UNDERLINE", + escape: escape!("4:5"), + }, + Metadata { + name: "BLINK", + escape: escape!("5"), + }, + Metadata { + name: "INVERT", + escape: escape!("7"), + }, + Metadata { + name: "HIDDEN", + escape: escape!("8"), + }, + Metadata { + name: "STRIKETHROUGH", + escape: escape!("9"), + }, +]; + +struct EffectsDisplay(Effects); + +impl core::fmt::Display for EffectsDisplay { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + for index in self.0.index_iter() { + METADATA[index].escape.fmt(f)?; + } + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EffectIter { + index: usize, + effects: Effects, +} + +impl Iterator for EffectIter { + type Item = Effects; + + fn next(&mut self) -> Option { + while self.index < METADATA.len() { + let index = self.index; + self.index += 1; + + let effect = Effects(1 << index); + if self.effects.contains(effect) { + return Some(effect); + } + } + + None + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct EffectIndexIter { + index: usize, + effects: Effects, +} + +impl Iterator for EffectIndexIter { + type Item = usize; + + fn next(&mut self) -> Option { + while self.index < METADATA.len() { + let index = self.index; + self.index += 1; + + let effect = Effects(1 << index); + if self.effects.contains(effect) { + return Some(index); + } + } + + None + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..41b7174 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,59 @@ +//! ANSI Text Styling +//! +//! *A portmanteau of "ansi style"* +//! +//! `anstyle` provides core types describing [ANSI styling escape +//! codes](https://en.wikipedia.org/wiki/ANSI_escape_code) for interoperability +//! between crates. +//! +//! Example use cases: +//! - An argument parser allowing callers to define the colors used in the help-output without +//! putting the text formatting crate in the public API +//! - A style description parser that can work with any text formatting crate +//! +//! Priorities: +//! 1. API stability +//! 2. Low compile-time and binary-size overhead +//! 3. `const` friendly API for callers to statically define their stylesheet +//! +//! For integration with text styling crate, see: +//! - [anstyle-ansi-term](https://docs.rs/anstyle-ansi-term) +//! - [anstyle-crossterm](https://docs.rs/anstyle-crossterm) +//! - [anstyle-owo-colors](https://docs.rs/anstyle-owo-colors) +//! - [anstyle-termcolor](https://docs.rs/anstyle-termcolor) +//! - [anstyle-yansi](https://docs.rs/anstyle-yansi) +//! +//! User-styling parsers: +//! - [anstyle-git](https://docs.rs/anstyle-git): Parse Git style descriptions +//! - [anstyle-ls](https://docs.rs/anstyle-ls): Parse LS_COLORS style descriptions +//! +//! Convert to other formats +//! - [anstream](https://docs.rs/anstream): A simple cross platform library for writing colored text to a terminal +//! - [anstyle-roff](https://docs.rs/anstyle-roff): For converting to ROFF +//! +//! Utilities +//! - [anstyle-lossy](https://docs.rs/anstyle-lossy): Convert between `anstyle::Color` types +//! - [anstyle-parse](https://docs.rs/anstyle-parse): Parsing ANSI Style Escapes +//! - [anstyle-wincon](https://docs.rs/anstyle-wincon): Styling legacy Microsoft terminals +//! +//! # Examples +//! +//! The core type is [`Style`]: +//! ```rust +//! let style = anstyle::Style::new().bold(); +//! ``` + +#![cfg_attr(not(feature = "std"), no_std)] + +#[macro_use] +mod macros; + +mod color; +mod effect; +mod reset; +mod style; + +pub use color::*; +pub use effect::*; +pub use reset::*; +pub use style::*; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..f19666e --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,5 @@ +macro_rules! escape { + ($($inner:expr),*) => { + concat!("\x1B[", $($inner),*, "m") + }; +} diff --git a/src/reset.rs b/src/reset.rs new file mode 100644 index 0000000..5f5f2b4 --- /dev/null +++ b/src/reset.rs @@ -0,0 +1,21 @@ +/// Reset terminal formatting +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Reset; + +impl Reset { + /// Render the ANSI code + #[inline] + pub fn render(self) -> impl core::fmt::Display { + ResetDisplay + } +} + +struct ResetDisplay; + +impl core::fmt::Display for ResetDisplay { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + RESET.fmt(f) + } +} + +pub(crate) const RESET: &str = "\x1B[0m"; diff --git a/src/style.rs b/src/style.rs new file mode 100644 index 0000000..623d7c7 --- /dev/null +++ b/src/style.rs @@ -0,0 +1,469 @@ +use crate::reset::RESET; + +/// ANSI Text styling +/// +/// # Examples +/// +/// ```rust +/// let style = anstyle::Style::new().bold(); +/// ``` +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Style { + fg: Option, + bg: Option, + underline: Option, + effects: crate::Effects, +} + +/// # Core +impl Style { + /// No effects enabled + /// + /// # Examples + /// + /// ```rust + /// let style = anstyle::Style::new(); + /// ``` + #[inline] + pub const fn new() -> Self { + Self { + fg: None, + bg: None, + underline: None, + effects: crate::Effects::new(), + } + } + + /// Set foreground color + /// + /// # Examples + /// + /// ```rust + /// let style = anstyle::Style::new().fg_color(Some(anstyle::AnsiColor::Red.into())); + /// ``` + #[must_use] + #[inline] + pub const fn fg_color(mut self, fg: Option) -> Self { + self.fg = fg; + self + } + + /// Set background color + /// + /// # Examples + /// + /// ```rust + /// let style = anstyle::Style::new().bg_color(Some(anstyle::AnsiColor::Red.into())); + /// ``` + #[must_use] + #[inline] + pub const fn bg_color(mut self, bg: Option) -> Self { + self.bg = bg; + self + } + + /// Set underline color + /// + /// # Examples + /// + /// ```rust + /// let style = anstyle::Style::new().underline_color(Some(anstyle::AnsiColor::Red.into())); + /// ``` + #[must_use] + #[inline] + pub const fn underline_color(mut self, underline: Option) -> Self { + self.underline = underline; + self + } + + /// Set text effects + /// + /// # Examples + /// + /// ```rust + /// let style = anstyle::Style::new().effects(anstyle::Effects::BOLD | anstyle::Effects::UNDERLINE); + /// ``` + #[must_use] + #[inline] + pub const fn effects(mut self, effects: crate::Effects) -> Self { + self.effects = effects; + self + } + + /// Render the ANSI code + #[inline] + pub fn render(self) -> impl core::fmt::Display { + StyleDisplay(self) + } + + /// Write the ANSI code + #[inline] + #[cfg(feature = "std")] + pub fn write_to(self, write: &mut dyn std::io::Write) -> std::io::Result<()> { + self.effects.write_to(write)?; + + if let Some(fg) = self.fg { + fg.write_fg_to(write)?; + } + + if let Some(bg) = self.bg { + bg.write_bg_to(write)?; + } + + if let Some(underline) = self.underline { + underline.write_underline_to(write)?; + } + + Ok(()) + } + + /// Renders the relevant [`Reset`][crate::Reset] code + /// + /// Unlike [`Reset::render`][crate::Reset::render], this will elide the code if there is nothing to reset. + #[inline] + pub fn render_reset(self) -> impl core::fmt::Display { + if self != Self::new() { + RESET + } else { + "" + } + } + + /// Write the relevant [`Reset`][crate::Reset] code + /// + /// Unlike [`Reset::render`][crate::Reset::render], this will elide the code if there is nothing to reset. + #[inline] + #[cfg(feature = "std")] + pub fn write_reset_to(self, write: &mut dyn std::io::Write) -> std::io::Result<()> { + if self != Self::new() { + write.write_all(RESET.as_bytes()) + } else { + Ok(()) + } + } +} + +/// # Convenience +impl Style { + /// Apply `bold` effect + /// + /// # Examples + /// + /// ```rust + /// let style = anstyle::Style::new().bold(); + /// ``` + #[must_use] + #[inline] + pub const fn bold(mut self) -> Self { + self.effects = self.effects.insert(crate::Effects::BOLD); + self + } + + /// Apply `dimmed` effect + /// + /// # Examples + /// + /// ```rust + /// let style = anstyle::Style::new().dimmed(); + /// ``` + #[must_use] + #[inline] + pub const fn dimmed(mut self) -> Self { + self.effects = self.effects.insert(crate::Effects::DIMMED); + self + } + + /// Apply `italic` effect + /// + /// # Examples + /// + /// ```rust + /// let style = anstyle::Style::new().italic(); + /// ``` + #[must_use] + #[inline] + pub const fn italic(mut self) -> Self { + self.effects = self.effects.insert(crate::Effects::ITALIC); + self + } + + /// Apply `underline` effect + /// + /// # Examples + /// + /// ```rust + /// let style = anstyle::Style::new().underline(); + /// ``` + #[must_use] + #[inline] + pub const fn underline(mut self) -> Self { + self.effects = self.effects.insert(crate::Effects::UNDERLINE); + self + } + + /// Apply `blink` effect + /// + /// # Examples + /// + /// ```rust + /// let style = anstyle::Style::new().blink(); + /// ``` + #[must_use] + #[inline] + pub const fn blink(mut self) -> Self { + self.effects = self.effects.insert(crate::Effects::BLINK); + self + } + + /// Apply `invert` effect + /// + /// # Examples + /// + /// ```rust + /// let style = anstyle::Style::new().invert(); + /// ``` + #[must_use] + #[inline] + pub const fn invert(mut self) -> Self { + self.effects = self.effects.insert(crate::Effects::INVERT); + self + } + + /// Apply `hidden` effect + /// + /// # Examples + /// + /// ```rust + /// let style = anstyle::Style::new().hidden(); + /// ``` + #[must_use] + #[inline] + pub const fn hidden(mut self) -> Self { + self.effects = self.effects.insert(crate::Effects::HIDDEN); + self + } + + /// Apply `strikethrough` effect + /// + /// # Examples + /// + /// ```rust + /// let style = anstyle::Style::new().strikethrough(); + /// ``` + #[must_use] + #[inline] + pub const fn strikethrough(mut self) -> Self { + self.effects = self.effects.insert(crate::Effects::STRIKETHROUGH); + self + } +} + +/// # Reflection +impl Style { + #[inline] + pub const fn get_fg_color(self) -> Option { + self.fg + } + + #[inline] + pub const fn get_bg_color(self) -> Option { + self.bg + } + + #[inline] + pub const fn get_underline_color(self) -> Option { + self.underline + } + + #[inline] + pub const fn get_effects(self) -> crate::Effects { + self.effects + } + + /// Check if no effects are enabled + #[inline] + pub const fn is_plain(self) -> bool { + self.fg.is_none() + && self.bg.is_none() + && self.underline.is_none() + && self.effects.is_plain() + } +} + +/// Define style with specified foreground color +/// +/// # Examples +/// +/// ```rust +/// let style: anstyle::Style = anstyle::Color::from((0, 0, 0)).into(); +/// ``` +impl From for Style { + #[inline] + fn from(color: crate::Color) -> Self { + Self::new().fg_color(Some(color)) + } +} + +/// Define style with specified foreground color +/// +/// # Examples +/// +/// ```rust +/// let style: anstyle::Style = anstyle::AnsiColor::Black.into(); +/// ``` +impl From for Style { + #[inline] + fn from(color: crate::AnsiColor) -> Self { + Self::new().fg_color(Some(color.into())) + } +} + +/// Define style with specified foreground color +/// +/// # Examples +/// +/// ```rust +/// let style: anstyle::Style = anstyle::Ansi256Color(0).into(); +/// ``` +impl From for Style { + #[inline] + fn from(color: crate::Ansi256Color) -> Self { + Self::new().fg_color(Some(color.into())) + } +} + +/// Define style with specified foreground color +/// +/// # Examples +/// +/// ```rust +/// let style: anstyle::Style = anstyle::RgbColor(0, 0, 0).into(); +/// ``` +impl From for Style { + #[inline] + fn from(color: crate::RgbColor) -> Self { + Self::new().fg_color(Some(color.into())) + } +} + +/// # Examples +/// +/// ```rust +/// let style: anstyle::Style = anstyle::Effects::BOLD.into(); +/// ``` +impl From for Style { + #[inline] + fn from(effects: crate::Effects) -> Self { + Self::new().effects(effects) + } +} + +/// # Examples +/// +/// ```rust +/// let style = anstyle::Style::new() | anstyle::Effects::BOLD.into(); +/// ``` +impl core::ops::BitOr for Style { + type Output = Self; + + #[inline(always)] + fn bitor(mut self, rhs: crate::Effects) -> Self { + self.effects |= rhs; + self + } +} + +/// # Examples +/// +/// ```rust +/// let mut style = anstyle::Style::new(); +/// style |= anstyle::Effects::BOLD.into(); +/// ``` +impl core::ops::BitOrAssign for Style { + #[inline] + fn bitor_assign(&mut self, other: crate::Effects) { + self.effects |= other; + } +} + +/// # Examples +/// +/// ```rust +/// let style = anstyle::Style::new().bold().underline() - anstyle::Effects::BOLD.into(); +/// ``` +impl core::ops::Sub for Style { + type Output = Self; + + #[inline] + fn sub(mut self, other: crate::Effects) -> Self { + self.effects -= other; + self + } +} + +/// # Examples +/// +/// ```rust +/// let mut style = anstyle::Style::new().bold().underline(); +/// style -= anstyle::Effects::BOLD.into(); +/// ``` +impl core::ops::SubAssign for Style { + #[inline] + fn sub_assign(&mut self, other: crate::Effects) { + self.effects -= other; + } +} + +/// # Examples +/// +/// ```rust +/// let color = anstyle::RgbColor(0, 0, 0); +/// assert_eq!(anstyle::Style::new().fg_color(Some(color.into())), color); +/// assert_ne!(color | anstyle::Effects::BOLD, color); +/// ``` +impl + Clone> core::cmp::PartialEq for Style { + #[inline] + fn eq(&self, other: &C) -> bool { + let other = other.clone().into(); + let other = Self::from(other); + *self == other + } +} + +/// # Examples +/// +/// ```rust +/// let effects = anstyle::Effects::BOLD; +/// assert_eq!(anstyle::Style::new().effects(effects), effects); +/// assert_ne!(anstyle::Effects::UNDERLINE | effects, effects); +/// assert_ne!(anstyle::RgbColor(0, 0, 0) | effects, effects); +/// ``` +impl core::cmp::PartialEq for Style { + #[inline] + fn eq(&self, other: &crate::Effects) -> bool { + let other = Self::from(*other); + *self == other + } +} + +struct StyleDisplay(Style); + +impl core::fmt::Display for StyleDisplay { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.effects.render().fmt(f)?; + + if let Some(fg) = self.0.fg { + fg.render_fg().fmt(f)?; + } + + if let Some(bg) = self.0.bg { + bg.render_bg().fmt(f)?; + } + + if let Some(underline) = self.0.underline { + underline.render_underline().fmt(f)?; + } + + Ok(()) + } +} -- 2.7.4