├── .gitignore ├── deno.json ├── .rustfmt.toml ├── README.md ├── rust-toolchain.toml ├── src ├── lib.rs └── colors.rs ├── Cargo.toml ├── .github └── workflows │ ├── release.yml │ └── ci.yml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | /target 3 | /Cargo.lock 4 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "target/" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | tab_spaces = 2 3 | edition = "2021" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deno_terminal 2 | 3 | Terminal styling and other functionality used in Deno. 4 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.75.0" 3 | components = [ "clippy", "rustfmt" ] -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | use std::io::IsTerminal; 4 | 5 | use once_cell::sync::Lazy; 6 | 7 | #[cfg(feature = "colors")] 8 | pub mod colors; 9 | 10 | static IS_STDOUT_TTY: Lazy = 11 | Lazy::new(|| std::io::stdout().is_terminal()); 12 | static IS_STDERR_TTY: Lazy = 13 | Lazy::new(|| std::io::stderr().is_terminal()); 14 | 15 | pub fn is_stdout_tty() -> bool { 16 | *IS_STDOUT_TTY 17 | } 18 | 19 | pub fn is_stderr_tty() -> bool { 20 | *IS_STDERR_TTY 21 | } 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deno_terminal" 3 | version = "0.2.3" 4 | edition = "2021" 5 | description = "Terminal styling and other functionality used across Deno" 6 | repository = "https://github.com/denoland/deno_terminal" 7 | documentation = "https://docs.rs/deno_terminal" 8 | authors = ["the Deno authors"] 9 | license = "MIT" 10 | 11 | [features] 12 | default = ["colors"] 13 | colors = ["termcolor"] 14 | 15 | [dependencies] 16 | once_cell = "1.17.1" 17 | termcolor = { version = "1.1.3", optional = true } 18 | 19 | [dev-dependencies] 20 | pretty_assertions = "1.0.0" 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | releaseKind: 7 | description: 'Kind of release' 8 | default: 'minor' 9 | type: choice 10 | options: 11 | - patch 12 | - minor 13 | required: true 14 | 15 | jobs: 16 | rust: 17 | name: release 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 30 20 | 21 | steps: 22 | - name: Clone repository 23 | uses: actions/checkout@v4 24 | with: 25 | token: ${{ secrets.DENOBOT_PAT }} 26 | 27 | - uses: denoland/setup-deno@v1 28 | - uses: dsherret/rust-toolchain-file@v1 29 | 30 | - name: Tag and release 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.DENOBOT_PAT }} 33 | GH_WORKFLOW_ACTOR: ${{ github.actor }} 34 | run: | 35 | git config user.email "denobot@users.noreply.github.com" 36 | git config user.name "denobot" 37 | deno run -A https://raw.githubusercontent.com/denoland/automation/0.14.2/tasks/publish_release.ts --${{github.event.inputs.releaseKind}} 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2024 the Deno authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | rust: 7 | name: deno_terminal-${{ matrix.os}} 8 | runs-on: ${{ matrix.os }} 9 | timeout-minutes: 30 10 | strategy: 11 | matrix: 12 | os: [macOS-latest, ubuntu-latest, windows-2025] 13 | 14 | env: 15 | CARGO_INCREMENTAL: 0 16 | GH_ACTIONS: 1 17 | RUST_BACKTRACE: full 18 | RUSTFLAGS: -D warnings 19 | 20 | steps: 21 | - name: Clone repository 22 | uses: actions/checkout@v4 23 | 24 | - name: Install rust 25 | uses: dsherret/rust-toolchain-file@v1 26 | 27 | - uses: Swatinem/rust-cache@v2 28 | with: 29 | save-if: ${{ github.ref == 'refs/heads/main' }} 30 | 31 | - name: Install Deno 32 | uses: denoland/setup-deno@v1 33 | 34 | - name: Format 35 | if: contains(matrix.os, 'ubuntu') 36 | run: | 37 | cargo fmt -- --check 38 | deno fmt --check 39 | 40 | - name: Lint 41 | if: contains(matrix.os, 'ubuntu') 42 | run: cargo clippy --all-features --all-targets -- -D clippy::all 43 | 44 | - name: Cargo Build 45 | run: cargo build --all-features --all-targets 46 | 47 | - name: Cargo Test 48 | run: cargo test --all-features --all-targets 49 | 50 | - name: Cargo publish 51 | if: | 52 | github.repository == 'denoland/deno_terminal' && 53 | startsWith(github.ref, 'refs/tags/') && 54 | contains(matrix.os, 'ubuntu') 55 | env: 56 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 57 | run: cargo publish 58 | -------------------------------------------------------------------------------- /src/colors.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | use once_cell::sync::Lazy; 4 | use std::fmt; 5 | use std::fmt::Write as _; 6 | use std::sync::atomic::AtomicBool; 7 | use termcolor::Ansi; 8 | use termcolor::Color::Ansi256; 9 | use termcolor::Color::Black; 10 | use termcolor::Color::Blue; 11 | use termcolor::Color::Cyan; 12 | use termcolor::Color::Green; 13 | use termcolor::Color::Magenta; 14 | use termcolor::Color::Red; 15 | use termcolor::Color::White; 16 | use termcolor::Color::Yellow; 17 | use termcolor::ColorSpec; 18 | use termcolor::WriteColor; 19 | 20 | #[cfg(windows)] 21 | use termcolor::BufferWriter; 22 | #[cfg(windows)] 23 | use termcolor::ColorChoice; 24 | 25 | static FORCE_COLOR: Lazy = Lazy::new(|| { 26 | std::env::var_os("FORCE_COLOR") 27 | .map(|v| !v.is_empty()) 28 | .unwrap_or(false) 29 | }); 30 | 31 | static USE_COLOR: Lazy = Lazy::new(|| { 32 | #[cfg(wasm)] 33 | { 34 | // Don't use color by default on Wasm targets because 35 | // it's not always possible to read the `NO_COLOR` env var. 36 | // 37 | // Instead the user can opt-in via `set_use_color`. 38 | AtomicBool::new(false) 39 | } 40 | #[cfg(not(wasm))] 41 | { 42 | if *FORCE_COLOR { 43 | return AtomicBool::new(true); 44 | } 45 | 46 | let no_color = std::env::var_os("NO_COLOR") 47 | .map(|v| !v.is_empty()) 48 | .unwrap_or(false); 49 | AtomicBool::new(!no_color) 50 | } 51 | }); 52 | 53 | #[derive(Clone, Copy, Debug)] 54 | pub enum ColorLevel { 55 | None, 56 | Ansi, 57 | Ansi256, 58 | TrueColor, 59 | } 60 | 61 | static COLOR_LEVEL: Lazy = Lazy::new(|| { 62 | #[cfg(wasm)] 63 | { 64 | // Don't use color by default on Wasm targets because 65 | // it's not always possible to read env vars. 66 | ColorLevel::None 67 | } 68 | #[cfg(not(wasm))] 69 | { 70 | // If FORCE_COLOR is not set, check for NO_COLOR 71 | if !*FORCE_COLOR { 72 | let no_color = std::env::var_os("NO_COLOR") 73 | .map(|v| !v.is_empty()) 74 | .unwrap_or(false); 75 | 76 | if no_color { 77 | return ColorLevel::None; 78 | } 79 | } 80 | 81 | let mut term = "dumb".to_string(); 82 | if let Some(term_value) = std::env::var_os("TERM") { 83 | // If FORCE_COLOR is set, ignore the "dumb" terminal check 84 | if term_value == "dumb" && !*FORCE_COLOR { 85 | return ColorLevel::None; 86 | } 87 | 88 | if let Some(term_value) = term_value.to_str() { 89 | term = term_value.to_string(); 90 | } 91 | } 92 | 93 | #[cfg(platform = "windows")] 94 | { 95 | // TODO: Older versions of windows only support ansi colors, 96 | // starting with Windows 10 build 10586 ansi256 is supported 97 | 98 | // TrueColor support landed with Windows 10 build 14931, 99 | // see https://devblogs.microsoft.com/commandline/24-bit-color-in-the-windows-console/ 100 | return ColorLevel::TrueColor; 101 | } 102 | 103 | if std::env::var_os("TMUX").is_some() { 104 | return ColorLevel::Ansi; 105 | } 106 | 107 | if let Some(ci) = std::env::var_os("CI") { 108 | if let Some(ci_value) = ci.to_str() { 109 | if matches!( 110 | ci_value, 111 | "TRAVIS" 112 | | "CIRCLECI" 113 | | "APPVEYOR" 114 | | "GITLAB_CI" 115 | | "GITHUB_ACTIONS" 116 | | "BUILDKITE" 117 | | "DRONE" 118 | ) { 119 | return ColorLevel::Ansi256; 120 | } 121 | } else { 122 | return ColorLevel::Ansi; 123 | } 124 | } 125 | 126 | if let Some(color_term) = std::env::var_os("COLORTERM") { 127 | if color_term == "truecolor" || color_term == "24bit" { 128 | return ColorLevel::TrueColor; 129 | } 130 | } 131 | 132 | if term.ends_with("-256color") || term.ends_with("256") { 133 | return ColorLevel::Ansi256; 134 | } 135 | 136 | ColorLevel::Ansi 137 | } 138 | }); 139 | 140 | pub fn get_color_level() -> ColorLevel { 141 | *COLOR_LEVEL 142 | } 143 | 144 | /// Gets whether color should be used in the output. 145 | /// 146 | /// This is determined by: 147 | /// - The `FORCE_COLOR` environment variable (if set, enables color regardless of other settings) 148 | /// - The `NO_COLOR` environment variable (if set and FORCE_COLOR is not set, disables color) 149 | /// - If `set_use_color` has been called to explicitly set the color state 150 | /// 151 | /// On Wasm targets, use `set_use_color(true)` to enable color output. 152 | pub fn use_color() -> bool { 153 | USE_COLOR.load(std::sync::atomic::Ordering::Relaxed) 154 | } 155 | 156 | /// Whether the `FORCE_COLOR` environment variable is set 157 | pub fn force_color() -> bool { 158 | *FORCE_COLOR 159 | } 160 | 161 | /// Sets whether color should be used in the output. 162 | /// 163 | /// This overrides the default values set via the `FORCE_COLOR` and `NO_COLOR` env vars. 164 | pub fn set_use_color(use_color: bool) { 165 | USE_COLOR.store(use_color, std::sync::atomic::Ordering::Relaxed); 166 | } 167 | 168 | /// Enables ANSI color output on Windows. This is a no-op on other platforms. 169 | pub fn enable_ansi() { 170 | #[cfg(windows)] 171 | { 172 | BufferWriter::stdout(ColorChoice::AlwaysAnsi); 173 | } 174 | } 175 | 176 | /// A struct that can adapt a `fmt::Write` to a `std::io::Write`. If anything 177 | /// that can not be represented as UTF-8 is written to this writer, it will 178 | /// return an error. 179 | struct StdFmtStdIoWriter<'a>(&'a mut dyn fmt::Write); 180 | 181 | impl std::io::Write for StdFmtStdIoWriter<'_> { 182 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 183 | let str = std::str::from_utf8(buf).map_err(|_| { 184 | std::io::Error::new( 185 | std::io::ErrorKind::Other, 186 | "failed to convert bytes to str", 187 | ) 188 | })?; 189 | match self.0.write_str(str) { 190 | Ok(_) => Ok(buf.len()), 191 | Err(_) => Err(std::io::Error::new( 192 | std::io::ErrorKind::Other, 193 | "failed to write to fmt::Write", 194 | )), 195 | } 196 | } 197 | 198 | fn flush(&mut self) -> std::io::Result<()> { 199 | Ok(()) 200 | } 201 | } 202 | 203 | /// A struct that can adapt a `std::io::Write` to a `fmt::Write`. 204 | struct StdIoStdFmtWriter<'a>(&'a mut dyn std::io::Write); 205 | 206 | impl fmt::Write for StdIoStdFmtWriter<'_> { 207 | fn write_str(&mut self, s: &str) -> fmt::Result { 208 | self.0.write_all(s.as_bytes()).map_err(|_| fmt::Error)?; 209 | Ok(()) 210 | } 211 | } 212 | 213 | pub struct Style { 214 | colorspec: ColorSpec, 215 | inner: I, 216 | } 217 | 218 | impl fmt::Display for Style { 219 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 220 | if !use_color() { 221 | return fmt::Display::fmt(&self.inner, f); 222 | } 223 | let mut ansi_writer = Ansi::new(StdFmtStdIoWriter(f)); 224 | ansi_writer 225 | .set_color(&self.colorspec) 226 | .map_err(|_| fmt::Error)?; 227 | write!(StdIoStdFmtWriter(&mut ansi_writer), "{}", self.inner)?; 228 | ansi_writer.reset().map_err(|_| fmt::Error)?; 229 | Ok(()) 230 | } 231 | } 232 | 233 | #[inline] 234 | fn style<'a, S: fmt::Display + 'a>(s: S, colorspec: ColorSpec) -> Style { 235 | Style { 236 | colorspec, 237 | inner: s, 238 | } 239 | } 240 | 241 | pub fn red_bold<'a, S: fmt::Display + 'a>(s: S) -> Style { 242 | let mut style_spec = ColorSpec::new(); 243 | style_spec.set_fg(Some(Red)).set_bold(true); 244 | style(s, style_spec) 245 | } 246 | 247 | pub fn green_bold<'a, S: fmt::Display + 'a>(s: S) -> Style { 248 | let mut style_spec = ColorSpec::new(); 249 | style_spec.set_fg(Some(Green)).set_bold(true); 250 | style(s, style_spec) 251 | } 252 | 253 | pub fn yellow_bold<'a, S: fmt::Display + 'a>(s: S) -> Style { 254 | let mut style_spec = ColorSpec::new(); 255 | style_spec.set_fg(Some(Yellow)).set_bold(true); 256 | style(s, style_spec) 257 | } 258 | 259 | pub fn italic<'a, S: fmt::Display + 'a>(s: S) -> Style { 260 | let mut style_spec = ColorSpec::new(); 261 | style_spec.set_italic(true); 262 | style(s, style_spec) 263 | } 264 | 265 | pub fn italic_gray<'a, S: fmt::Display + 'a>(s: S) -> Style { 266 | let mut style_spec = ColorSpec::new(); 267 | style_spec.set_fg(Some(Ansi256(8))).set_italic(true); 268 | style(s, style_spec) 269 | } 270 | 271 | pub fn italic_bold<'a, S: fmt::Display + 'a>(s: S) -> Style { 272 | let mut style_spec = ColorSpec::new(); 273 | style_spec.set_bold(true).set_italic(true); 274 | style(s, style_spec) 275 | } 276 | 277 | pub fn white_on_red<'a, S: fmt::Display + 'a>(s: S) -> Style { 278 | let mut style_spec = ColorSpec::new(); 279 | style_spec.set_bg(Some(Red)).set_fg(Some(White)); 280 | style(s, style_spec) 281 | } 282 | 283 | pub fn black_on_green<'a, S: fmt::Display + 'a>(s: S) -> Style { 284 | let mut style_spec = ColorSpec::new(); 285 | style_spec.set_bg(Some(Green)).set_fg(Some(Black)); 286 | style(s, style_spec) 287 | } 288 | 289 | pub fn yellow<'a, S: fmt::Display + 'a>(s: S) -> Style { 290 | let mut style_spec = ColorSpec::new(); 291 | style_spec.set_fg(Some(Yellow)); 292 | style(s, style_spec) 293 | } 294 | 295 | pub fn cyan<'a, S: fmt::Display + 'a>(s: S) -> Style { 296 | let mut style_spec = ColorSpec::new(); 297 | style_spec.set_fg(Some(Cyan)); 298 | style(s, style_spec) 299 | } 300 | 301 | pub fn cyan_with_underline<'a>( 302 | s: impl fmt::Display + 'a, 303 | ) -> impl fmt::Display + 'a { 304 | let mut style_spec = ColorSpec::new(); 305 | style_spec.set_fg(Some(Cyan)).set_underline(true); 306 | style(s, style_spec) 307 | } 308 | 309 | pub fn cyan_bold<'a, S: fmt::Display + 'a>(s: S) -> Style { 310 | let mut style_spec = ColorSpec::new(); 311 | style_spec.set_fg(Some(Cyan)).set_bold(true); 312 | style(s, style_spec) 313 | } 314 | 315 | pub fn magenta<'a, S: fmt::Display + 'a>(s: S) -> Style { 316 | let mut style_spec = ColorSpec::new(); 317 | style_spec.set_fg(Some(Magenta)); 318 | style(s, style_spec) 319 | } 320 | 321 | pub fn red<'a, S: fmt::Display + 'a>(s: S) -> Style { 322 | let mut style_spec = ColorSpec::new(); 323 | style_spec.set_fg(Some(Red)); 324 | style(s, style_spec) 325 | } 326 | 327 | pub fn green<'a, S: fmt::Display + 'a>(s: S) -> Style { 328 | let mut style_spec = ColorSpec::new(); 329 | style_spec.set_fg(Some(Green)); 330 | style(s, style_spec) 331 | } 332 | 333 | pub fn bold<'a, S: fmt::Display + 'a>(s: S) -> Style { 334 | let mut style_spec = ColorSpec::new(); 335 | style_spec.set_bold(true); 336 | style(s, style_spec) 337 | } 338 | 339 | pub fn gray<'a, S: fmt::Display + 'a>(s: S) -> Style { 340 | let mut style_spec = ColorSpec::new(); 341 | style_spec.set_fg(Some(Ansi256(245))); 342 | style(s, style_spec) 343 | } 344 | 345 | pub fn dimmed_gray<'a, S: fmt::Display + 'a>(s: S) -> Style { 346 | let mut style_spec = ColorSpec::new(); 347 | style_spec.set_dimmed(true).set_fg(Some(Ansi256(245))); 348 | style(s, style_spec) 349 | } 350 | 351 | pub fn intense_blue<'a, S: fmt::Display + 'a>(s: S) -> Style { 352 | let mut style_spec = ColorSpec::new(); 353 | style_spec.set_fg(Some(Blue)).set_intense(true); 354 | style(s, style_spec) 355 | } 356 | 357 | pub fn white_bold_on_red<'a>( 358 | s: impl fmt::Display + 'a, 359 | ) -> impl fmt::Display + 'a { 360 | let mut style_spec = ColorSpec::new(); 361 | style_spec 362 | .set_bold(true) 363 | .set_bg(Some(Red)) 364 | .set_fg(Some(White)); 365 | style(s, style_spec) 366 | } 367 | --------------------------------------------------------------------------------