diff options
-rw-r--r-- | .cargo_vcs_info.json | 6 | ||||
-rw-r--r-- | Cargo.toml | 95 | ||||
-rw-r--r-- | Cargo.toml.orig | 37 | ||||
-rw-r--r-- | LICENSE-APACHE | 202 | ||||
-rw-r--r-- | LICENSE-MIT | 19 | ||||
-rw-r--r-- | README.md | 44 | ||||
-rw-r--r-- | src/choice.rs | 14 | ||||
-rw-r--r-- | src/color/lazy.rs | 48 | ||||
-rw-r--r-- | src/color/mod.rs | 235 | ||||
-rw-r--r-- | src/lib.rs | 105 | ||||
-rw-r--r-- | src/no_color.rs | 26 | ||||
-rw-r--r-- | src/stream.rs | 8 |
12 files changed, 839 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..b4dd1f9 --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "37f8791a96edf8a5953d3f332b82d2d9edd02b50" + }, + "path_in_vcs": "crates/concolor" +}
\ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0b670d5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,95 @@ +# 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.60.0" +name = "concolor" +version = "0.1.1" +include = [ + "src/**/*", + "Cargo.toml", + "LICENSE*", + "README.md", + "examples/**/*", +] +description = "Control console coloring across all dependencies" +readme = "README.md" +keywords = [ + "cli", + "color", + "no-std", + "terminal", + "ansi", +] +categories = ["command-line-interface"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-cli/concolor" + +[package.metadata.docs.rs] +features = [ + "auto", + "api", +] + +[dependencies.bitflags] +version = "1" +optional = true + +[dependencies.concolor-override] +version = "1.0.0" +optional = true + +[dependencies.concolor-query] +version = "0.3.2" +optional = true + +[dependencies.is-terminal] +version = "0.4" +optional = true + +[features] +api = [ + "core", + "dep:concolor-override", +] +auto = [ + "interactive", + "clicolor", + "no_color", + "term", + "windows", +] +clicolor = [ + "core", + "dep:concolor-query", +] +core = [ + "std", + "dep:bitflags", +] +interactive = [ + "core", + "dep:is-terminal", +] +no_color = [ + "core", + "dep:concolor-query", +] +std = [] +term = [ + "core", + "dep:concolor-query", +] +windows = [ + "core", + "dep:concolor-query", +] diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..3854ce9 --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,37 @@ +[package] +name = "concolor" +version = "0.1.1" +license = "MIT OR Apache-2.0" +description = "Control console coloring across all dependencies" +repository = "https://github.com/rust-cli/concolor" +categories = ["command-line-interface"] +keywords = ["cli", "color", "no-std", "terminal", "ansi"] +edition = "2021" +rust-version = "1.60.0" # MSRV +include = [ + "src/**/*", + "Cargo.toml", + "LICENSE*", + "README.md", + "examples/**/*" +] + +[package.metadata.docs.rs] +features = ["auto", "api"] + +[features] +auto = ["interactive", "clicolor", "no_color", "term", "windows"] +std = [] +core = ["std", "dep:bitflags"] +api = ["core", "dep:concolor-override"] +interactive = ["core", "dep:is-terminal"] +clicolor = ["core", "dep:concolor-query"] +no_color = ["core", "dep:concolor-query"] +term = ["core", "dep:concolor-query"] +windows = ["core", "dep:concolor-query"] + +[dependencies] +concolor-override = { version = "1.0.0", path = "../override", optional = true } +concolor-query = { version = "0.3.2", path = "../query", optional = true } +bitflags = { version = "1", optional = true } +is-terminal = { version = "0.4", optional = true } 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..0182f12 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) 2019-2021 The typos 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..9598dc0 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# concolor + +> **bin/lib API for managing terminal styling** + +[![Documentation](https://img.shields.io/badge/docs-master-blue.svg)][Documentation] +![License](https://img.shields.io/crates/l/concolor.svg) +[![Crates Status](https://img.shields.io/crates/v/concolor.svg)](https://crates.io/crates/concolor) + +Features +- Detects interactive `stdout` / `stderr` +- Detects terminal capabilities via `TERM` +- Detects and enables ANSI support on Windows +- Supports [CLICOLOR] and [NO_COLOR] + +## [Contribute](../../CONTRIBUTING.md) + +Special note: to be successful, this crate **cannot** break compatibility or +else different crates in the hierarchy will be reading different globals. +While end users can work around this, it isn't ideal. Once we hit 1.0, we +should strive to keep the API compatible. If we need a new API, we can make +the old API an adapter to the new logic. + +Similarly, we should strive to reduce **risk** of breaking compatibility by +exposing as little as possible. Anything more should be broken out into a +separate crate that this crate can call into. + +## Special Thanks + +Prior art for global colors control: + +- [yansi](https://crates.io/crates/yansi) +- [clicolors-control](https://crates.io/crates/clicolors-control) + +[termcolor](https://crates.io/crates/termcolor) for identifying various corner cases with environment detection. + +[firestorm](https://crates.io/crates/firestorm) for zero-cost abstraction via bin/lib-specific `Cargo.toml` features. + +## License + +Dual-licensed under [MIT](../../LICENSE-MIT) or [Apache 2.0](../../LICENSE-APACHE) + +[Documentation]: https://docs.rs/concolor +[CLICOLOR]: https://bixense.com/clicolors/ +[NO_COLOR]: https://no-color.org/ diff --git a/src/choice.rs b/src/choice.rs new file mode 100644 index 0000000..0c8537c --- /dev/null +++ b/src/choice.rs @@ -0,0 +1,14 @@ +/// Selection for overriding color output with [`set`][crate::set] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ColorChoice { + Auto, + AlwaysAnsi, + Always, + Never, +} + +impl Default for ColorChoice { + fn default() -> Self { + Self::Auto + } +} diff --git a/src/color/lazy.rs b/src/color/lazy.rs new file mode 100644 index 0000000..0c59803 --- /dev/null +++ b/src/color/lazy.rs @@ -0,0 +1,48 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// Specialized once_cell::race::OnceNonZeroUsize +/// +/// Use MAX instead of MIN for uninit case to make it easier to work with for bitflags +#[derive(Debug)] +pub(crate) struct Lazy(AtomicUsize); + +impl Lazy { + const UNINIT: usize = usize::MAX; + + pub(crate) const fn new() -> Self { + Self(AtomicUsize::new(Self::UNINIT)) + } + + pub(crate) fn get_or_init<F>(&self, f: F) -> usize + where + F: FnOnce() -> usize, + { + self.get().unwrap_or_else(|| { + let mut val = f(); + assert_ne!(val, Self::UNINIT); + let exchange = + self.0 + .compare_exchange(Self::UNINIT, val, Ordering::AcqRel, Ordering::Acquire); + if let Err(old) = exchange { + val = old; + } + debug_assert_ne!(val, Self::UNINIT); + val + }) + } + + fn get(&self) -> Option<usize> { + let val = self.0.load(Ordering::Acquire); + if val == Self::UNINIT { + None + } else { + Some(val) + } + } +} + +impl Default for Lazy { + fn default() -> Self { + Lazy::new() + } +} diff --git a/src/color/mod.rs b/src/color/mod.rs new file mode 100644 index 0000000..6997d88 --- /dev/null +++ b/src/color/mod.rs @@ -0,0 +1,235 @@ +mod lazy; + +/// Current color state for a [`Stream`][crate::Stream] +/// +/// Note: if you hold onto this around calls to [`set`][crate::set], it will be inaccurate. +#[derive(Clone, Debug)] +pub struct Color { + flags: InternalFlags, + choice: crate::ColorChoice, + stream: crate::Stream, +} + +impl Color { + /// Should color be used? + /// + /// Note: if supporting wincon coloring, fallback to ANSI if this returns `true`. + pub fn color(&self) -> bool { + match self.choice { + crate::ColorChoice::Auto => self.flags.color(self.stream), + crate::ColorChoice::AlwaysAnsi => true, + crate::ColorChoice::Always => true, + crate::ColorChoice::Never => false, + } + } + + /// Should use ANSI coloring? + #[cfg(not(windows))] + pub fn ansi_color(&self) -> bool { + match self.choice { + crate::ColorChoice::Auto => self.flags.ansi_color(self.stream), + crate::ColorChoice::AlwaysAnsi => true, + crate::ColorChoice::Always => true, + crate::ColorChoice::Never => false, + } + } + + /// Should use ANSI coloring? + #[cfg(windows)] + pub fn ansi_color(&self) -> bool { + match self.choice { + crate::ColorChoice::Auto => self.flags.ansi_color(self.stream), + crate::ColorChoice::AlwaysAnsi => true, + crate::ColorChoice::Always => self.flags.intersects(InternalFlags::ANSI_ANY), + crate::ColorChoice::Never => false, + } + } + + /// Should use ANSI truecolor? + pub fn truecolor(&self) -> bool { + self.ansi_color() && self.flags.contains(InternalFlags::TRUECOLOR) + } +} + +static FLAGS: lazy::Lazy = lazy::Lazy::new(); + +/// Get the current [`Color`] state for a given [`Stream`][crate::Stream] +pub fn get(stream: crate::Stream) -> Color { + let flags = FLAGS.get_or_init(init); + #[cfg(feature = "api")] + let choice = match concolor_override::get() { + concolor_override::ColorChoice::Auto => crate::ColorChoice::Auto, + concolor_override::ColorChoice::AlwaysAnsi => crate::ColorChoice::AlwaysAnsi, + concolor_override::ColorChoice::Always => crate::ColorChoice::Always, + concolor_override::ColorChoice::Never => crate::ColorChoice::Never, + }; + #[cfg(not(feature = "api"))] + let choice = crate::ColorChoice::Auto; + Color { + flags: InternalFlags::from_bits(flags).unwrap(), + choice, + stream, + } +} + +/// Override the detected [`ColorChoice`][crate::ColorChoice] +#[cfg(feature = "api")] +pub fn set(choice: crate::ColorChoice) { + let choice = match choice { + crate::ColorChoice::Auto => concolor_override::ColorChoice::Auto, + crate::ColorChoice::AlwaysAnsi => concolor_override::ColorChoice::AlwaysAnsi, + crate::ColorChoice::Always => concolor_override::ColorChoice::Always, + crate::ColorChoice::Never => concolor_override::ColorChoice::Never, + }; + concolor_override::set(choice) +} + +fn init() -> usize { + let mut flags = InternalFlags::empty(); + + #[cfg(feature = "clicolor")] + { + if concolor_query::clicolor().unwrap_or(true) { + flags |= InternalFlags::CLICOLOR; + } + if concolor_query::clicolor_force() { + flags |= InternalFlags::CLICOLOR_FORCE; + } + } + #[cfg(not(feature = "clicolor"))] + { + // Spec defaults to enabled + flags |= InternalFlags::CLICOLOR; + } + + #[cfg(feature = "no_color")] + if concolor_query::no_color() { + flags |= InternalFlags::NO_COLOR; + } + + #[cfg(feature = "term")] + { + if concolor_query::term_supports_color() { + flags |= InternalFlags::TERM_SUPPORT; + } + if concolor_query::term_supports_ansi_color() { + flags |= InternalFlags::ANSI_SUPPORT; + } + if concolor_query::truecolor() { + flags |= InternalFlags::TRUECOLOR; + } + } + #[cfg(not(feature = "term"))] + { + // Don't block color on lack of `term` support, acting as if this field doesn't exist + flags |= InternalFlags::TERM_SUPPORT; + if cfg!(not(windows)) { + // Limit to non-windows platforms as windows natively support wincon instead of ANSI + flags |= InternalFlags::ANSI_SUPPORT; + } + } + + #[cfg(feature = "interactive")] + { + use is_terminal::IsTerminal; + use std::io::{stderr, stdout}; + if stdout().is_terminal() { + flags |= InternalFlags::TTY_STDOUT; + } + if stderr().is_terminal() { + flags |= InternalFlags::TTY_STDERR; + } + } + #[cfg(not(feature = "interactive"))] + { + // Don't block color on lack of `interactive` support, acting as if these fields doesn't + // exist + flags |= InternalFlags::TTY_STDOUT; + flags |= InternalFlags::TTY_STDERR; + } + + // No fallback when disabled since something has to enable ANSI support on Windows + #[cfg(feature = "windows")] + if concolor_query::windows::enable_ansi_colors().unwrap_or(false) { + flags |= InternalFlags::ANSI_WIN; + } + + flags.bits() +} + +bitflags::bitflags! { + struct InternalFlags: usize { + const CLICOLOR = 0b00000000001; + const CLICOLOR_FORCE = 0b00000000010; + const NO_COLOR = 0b00000000100; + const TERM_SUPPORT = 0b00000001000; + const ANSI_SUPPORT = 0b00000010000; + const ANSI_WIN = 0b00000100000; + const ANSI_ANY = 0b00000110000; + const TRUECOLOR = 0b00001000000; + const TTY_STDOUT = 0b00010000000; + const TTY_STDERR = 0b00100000000; + const TTY_ANY = 0b00110000000; + } +} + +impl InternalFlags { + fn color(self, stream: crate::Stream) -> bool { + (self.is_interactive(stream) + && self.contains(Self::TERM_SUPPORT) + && self.contains(Self::CLICOLOR) + && !self.contains(Self::NO_COLOR)) + || self.contains(Self::CLICOLOR_FORCE) + } + + fn ansi_color(self, stream: crate::Stream) -> bool { + (self.is_interactive(stream) + && self.contains(Self::TERM_SUPPORT) + && self.intersects(Self::ANSI_ANY) + && self.contains(Self::CLICOLOR) + && !self.contains(Self::NO_COLOR)) + || self.contains(Self::CLICOLOR_FORCE) + } + + fn is_interactive(self, stream: crate::Stream) -> bool { + self.contains(stream.flags()) + } +} + +impl crate::Stream { + fn flags(self) -> InternalFlags { + match self { + Self::Stdout => InternalFlags::TTY_STDOUT, + Self::Stderr => InternalFlags::TTY_STDERR, + Self::Either => InternalFlags::TTY_ANY, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn is_interactive() { + let flags = InternalFlags::empty(); + assert!(!flags.is_interactive(crate::Stream::Stdout)); + assert!(!flags.is_interactive(crate::Stream::Stderr)); + assert!(!flags.is_interactive(crate::Stream::Either)); + + let flags = InternalFlags::TTY_STDOUT; + assert!(flags.is_interactive(crate::Stream::Stdout)); + assert!(!flags.is_interactive(crate::Stream::Stderr)); + assert!(!flags.is_interactive(crate::Stream::Either)); + + let flags = InternalFlags::TTY_STDERR; + assert!(!flags.is_interactive(crate::Stream::Stdout)); + assert!(flags.is_interactive(crate::Stream::Stderr)); + assert!(!flags.is_interactive(crate::Stream::Either)); + + let flags = InternalFlags::TTY_STDOUT | InternalFlags::TTY_STDERR; + assert!(flags.is_interactive(crate::Stream::Stdout)); + assert!(flags.is_interactive(crate::Stream::Stderr)); + assert!(flags.is_interactive(crate::Stream::Either)); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..43e0348 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,105 @@ +//! Control console coloring across all dependencies +//! +//! # Motivation +//! +//! Detecting a terminal's color capabilities and passing it down to each writer +//! can be obnoxious. Some crates try to make this easier by detecting the environment for you and +//! making their own choice to print colors. As an application author, you own the experience for +//! your application and want the behavior to be consistent. To get this, you have to dig into +//! each crate's implementation to see how they auto-detect color capabilities and, if they don't +//! do it how you want, hope they provide a way to override it so you can implement it yourself. +//! +//! Like with logging, your terminal's capabilities and how to treat it is a behavior that cuts +//! across your application. So to make things more consistent and easier to control, +//! `concolor` introduces shared detection logic that all crates can call into to get +//! consistent behavior. The application author can then choose what feature flags are enabled to +//! decide on what the end-user experience should be. +//! +//! # `[[bin]]`s +//! +//! ```toml +//! [dependencies] +//! concolor = { version = "0.1.1", features = "color" } +//! ``` +//! Notes: +//! - With the +//! [2021 edition / `resolver = "2"`](https://doc.rust-lang.org/nightly/edition-guide/rust-2021/default-cargo-resolver.html), +//! you will also need to specify this in your `build-dependencies` if you want `build.rs` to have color +//! as well. +//! +//! If you are providing a command line option for controlling color, just call +//! ```rust +//! let when = concolor::ColorChoice::Always; +//! concolor::set(when); +//! ``` +//! +//! See also [`concolor-clap`](https://docs.rs/concolor-clap) +//! +//! # `[lib]`s +//! +//! The `[[bin]]` is responsible for defining the policy of how colors are determined, so to depend +//! on `concolor`: +//! ```toml +//! [dependencies] +//! concolor = { version = "0.1.1", default-features = false } +//! ``` +//! +//! At times, you might want to provide a convenience feature for color support, so you could also: +//! ```toml +//! [features] +//! default = ["color"] +//! color = "concolor/auto" +//! +//! [dependencies] +//! concolor = { version = "0.1.1", optional = True} +//! ``` +//! Notes: +//! - Your choice on whether to make this default or not +//! - Depending on your context, name it either `color` (for a crate like `clap`) or `auto` (for a +//! crate like `termcolor`) +//! +//! Then just ask as needed: +//! ```rust +//! let stdout_support = concolor::get(concolor::Stream::Stdout); +//! if stdout_support.ansi_color() { +//! // Output ANSI escape sequences +//! if stdout_support.truecolor() { +//! // Get even fancier with the colors +//! } +//! } else if stdout_support.color() { +//! // Legacy Windows version, control the console as needed +//! } else { +//! // No coloring +//! } +//! ``` +//! +//! # Features +//! +//! - `auto`: Guess color status based on all possible sources, including: +//! - `api_unstable`: Allow controlling color via the API (until 1.0, this is not guaranteed to +//! work across crates which is why this is `_unstable`) +//! - `interactive`: Check if stdout/stderr is a TTY +//! - `clicolor`: Respect [CLICOLOR] spec +//! - `no_color`: Respect [NO_COLOR] spec +//! - `term`: Check `TERM` +//! - `windows`: Check if we can enable ANSI support +//! +//! [CLICOLOR]: https://bixense.com/clicolors/ +//! [NO_COLOR]: https://no-color.org/ + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "core")] +mod color; +#[cfg(feature = "core")] +pub use color::*; + +#[cfg(not(feature = "core"))] +mod no_color; +#[cfg(not(feature = "core"))] +pub use no_color::*; + +mod choice; +pub use choice::*; +mod stream; +pub use stream::*; diff --git a/src/no_color.rs b/src/no_color.rs new file mode 100644 index 0000000..d5eb813 --- /dev/null +++ b/src/no_color.rs @@ -0,0 +1,26 @@ +#[derive(Clone, Debug)] +pub struct Color {} + +impl Color { + #[inline] + pub const fn truecolor(&self) -> bool { + false + } + + #[inline] + pub const fn color(&self) -> bool { + false + } + + #[inline] + pub const fn ansi_color(&self) -> bool { + false + } +} + +pub fn get(_stream: crate::Stream) -> Color { + Color {} +} + +#[cfg(feature = "api_unstable")] +pub fn set(_choice: crate::ColorChoice) {} diff --git a/src/stream.rs b/src/stream.rs new file mode 100644 index 0000000..ff28d3e --- /dev/null +++ b/src/stream.rs @@ -0,0 +1,8 @@ +/// Output stream to [`get()`][crate::get] the [`Color`][crate::Color] state for +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Stream { + Stdout, + Stderr, + /// When unsure which will be used (lowest common denominator of `Stdout` and `Stderr`) + Either, +} |