From cb86bc52265f9250e4902231ea42f282e760f4b6 Mon Sep 17 00:00:00 2001 From: James Bradlee Date: Mon, 1 Apr 2024 13:11:32 +0200 Subject: [PATCH] feat: terminal coloring module (#18) --- .../io/tnboot/logging/util/colorful/Code.kt | 23 ++ .../io/tnboot/logging/util/colorful/Codes.kt | 44 +++ .../logging/util/colorful/EnableState.kt | 33 ++ .../io/tnboot/logging/util/colorful/Mod.kt | 78 +++++ .../io/tnboot/logging/util/colorful/Stylus.kt | 14 + .../tnboot/logging/util/colorful/ModTests.kt | 309 ++++++++++++++++++ settings.gradle.kts | 4 + 7 files changed, 505 insertions(+) create mode 100644 projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Code.kt create mode 100644 projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Codes.kt create mode 100644 projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/EnableState.kt create mode 100644 projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Mod.kt create mode 100644 projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Stylus.kt create mode 100644 projects/logging/colorful/src/test/kotlin/io/tnboot/logging/util/colorful/ModTests.kt diff --git a/projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Code.kt b/projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Code.kt new file mode 100644 index 0000000..86e9293 --- /dev/null +++ b/projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Code.kt @@ -0,0 +1,23 @@ +package io.tnboot.logging.util.colorful + +/** Internal, don't touch, or you will be fired. */ +class Code private constructor( + private val open: String, + private val close: String, + private val regexp: Regex, +) : Stylus { + @OptIn(ExperimentalUnsignedTypes::class) + internal constructor(close: UByte, vararg open: UByte) : this( + "\u001b[${open.joinToString(";")}m", + "\u001b[${close}m", + Regex("\\u001b\\[${close}m"), + ) + + override fun style(str: String): String { + return if (isEnabled) { + "$open${str.replace(regexp, open)}$close" + } else { + str + } + } +} diff --git a/projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Codes.kt b/projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Codes.kt new file mode 100644 index 0000000..a99a9c8 --- /dev/null +++ b/projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Codes.kt @@ -0,0 +1,44 @@ +package io.tnboot.logging.util.colorful + +/* @formatter:off */ +@OptIn(ExperimentalUnsignedTypes::class) val RESET = Code(0U, 0U) +@OptIn(ExperimentalUnsignedTypes::class) val BOLD = Code(22U, 1U) +@OptIn(ExperimentalUnsignedTypes::class) val DIM = Code(22U, 2U) +@OptIn(ExperimentalUnsignedTypes::class) val ITALIC = Code(23U, 3U) +@OptIn(ExperimentalUnsignedTypes::class) val UNDERLINE = Code(24U, 4U) +@OptIn(ExperimentalUnsignedTypes::class) val INVERSE = Code(27U, 7U) +@OptIn(ExperimentalUnsignedTypes::class) val HIDDEN = Code(28U, 8U) +@OptIn(ExperimentalUnsignedTypes::class) val STRIKETHROUGH = Code(29U, 9U) +@OptIn(ExperimentalUnsignedTypes::class) val BLACK = Code(39U, 30U) +@OptIn(ExperimentalUnsignedTypes::class) val RED = Code(39U, 31U) +@OptIn(ExperimentalUnsignedTypes::class) val GREEN = Code(39U, 32U) +@OptIn(ExperimentalUnsignedTypes::class) val YELLOW = Code(39U, 33U) +@OptIn(ExperimentalUnsignedTypes::class) val BLUE = Code(39U, 34U) +@OptIn(ExperimentalUnsignedTypes::class) val MAGENTA = Code(39U, 35U) +@OptIn(ExperimentalUnsignedTypes::class) val CYAN = Code(39U, 36U) +@OptIn(ExperimentalUnsignedTypes::class) val WHITE = Code(39U, 37U) +@OptIn(ExperimentalUnsignedTypes::class) val BRIGHT_BLACK = Code(39U, 90U) +@OptIn(ExperimentalUnsignedTypes::class) val BRIGHT_RED = Code(39U, 91U) +@OptIn(ExperimentalUnsignedTypes::class) val BRIGHT_GREEN = Code(39U, 92U) +@OptIn(ExperimentalUnsignedTypes::class) val BRIGHT_YELLOW = Code(39U, 93U) +@OptIn(ExperimentalUnsignedTypes::class) val BRIGHT_BLUE = Code(39U, 94U) +@OptIn(ExperimentalUnsignedTypes::class) val BRIGHT_MAGENTA = Code(39U, 95U) +@OptIn(ExperimentalUnsignedTypes::class) val BRIGHT_CYAN = Code(39U, 96U) +@OptIn(ExperimentalUnsignedTypes::class) val BRIGHT_WHITE = Code(39U, 97U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_BLACK = Code(49U, 40U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_RED = Code(49U, 41U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_GREEN = Code(49U, 42U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_YELLOW = Code(49U, 43U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_BLUE = Code(49U, 44U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_MAGENTA = Code(49U, 45U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_CYAN = Code(49U, 46U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_WHITE = Code(49U, 47U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_BRIGHT_BLACK = Code(49U, 100U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_BRIGHT_RED = Code(49U, 101U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_BRIGHT_GREEN = Code(49U, 102U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_BRIGHT_YELLOW = Code(49U, 103U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_BRIGHT_BLUE = Code(49U, 104U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_BRIGHT_MAGENTA = Code(49U, 105U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_BRIGHT_CYAN = Code(49U, 106U) +@OptIn(ExperimentalUnsignedTypes::class) val BG_BRIGHT_WHITE = Code(49U, 107U) +/* @formatter:on */ diff --git a/projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/EnableState.kt b/projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/EnableState.kt new file mode 100644 index 0000000..124e5e1 --- /dev/null +++ b/projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/EnableState.kt @@ -0,0 +1,33 @@ +package io.tnboot.logging.util.colorful + +/** + * Internal object to store the colors enabled state. Used for thread + * safety. + */ +internal object Enabled { + /** Whether colors wil be rendered. */ + internal var enabled = System.getenv("NO_COLOR")?.let { false } ?: true +} + +/** Whether colors wil be rendered. */ +val isEnabled get() = synchronized(Enabled) { Enabled.enabled } + +/** Enable color rendering. */ +fun enable() = synchronized(Enabled) { + Enabled.enabled = true +} + +/** Disable color rendering. */ +fun disable() = synchronized(Enabled) { + Enabled.enabled = false +} + +/** Toggle the enabled state. */ +fun toggleEnabledState() = synchronized(Enabled) { + Enabled.enabled = !Enabled.enabled +} + +/** Set the enabled state. */ +fun setEnabledState(enabled: Boolean) = synchronized(Enabled) { + Enabled.enabled = enabled +} diff --git a/projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Mod.kt b/projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Mod.kt new file mode 100644 index 0000000..98054ec --- /dev/null +++ b/projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Mod.kt @@ -0,0 +1,78 @@ +package io.tnboot.logging.util.colorful + +fun reset(text: String) = RESET.style(text) +fun bold(text: String) = BOLD.style(text) +fun dim(text: String) = DIM.style(text) +fun italic(text: String) = ITALIC.style(text) +fun underline(text: String) = UNDERLINE.style(text) +fun inverse(text: String) = INVERSE.style(text) +fun hidden(text: String) = HIDDEN.style(text) +fun strikeThrough(text: String) = STRIKETHROUGH.style(text) +fun black(text: String) = BLACK.style(text) +fun red(text: String) = RED.style(text) +fun green(text: String) = GREEN.style(text) +fun yellow(text: String) = YELLOW.style(text) +fun blue(text: String) = BLUE.style(text) +fun magenta(text: String) = MAGENTA.style(text) +fun cyan(text: String) = CYAN.style(text) +fun white(text: String) = WHITE.style(text) +fun brightBlack(text: String) = BRIGHT_BLACK.style(text) +fun brightRed(text: String) = BRIGHT_RED.style(text) +fun brightGreen(text: String) = BRIGHT_GREEN.style(text) +fun brightYellow(text: String) = BRIGHT_YELLOW.style(text) +fun brightBlue(text: String) = BRIGHT_BLUE.style(text) +fun brightMagenta(text: String) = BRIGHT_MAGENTA.style(text) +fun brightCyan(text: String) = BRIGHT_CYAN.style(text) +fun brightWhite(text: String) = BRIGHT_WHITE.style(text) +fun bgBlack(text: String) = BG_BLACK.style(text) +fun bgRed(text: String) = BG_RED.style(text) +fun bgGreen(text: String) = BG_GREEN.style(text) +fun bgYellow(text: String) = BG_YELLOW.style(text) +fun bgBlue(text: String) = BG_BLUE.style(text) +fun bgMagenta(text: String) = BG_MAGENTA.style(text) +fun bgCyan(text: String) = BG_CYAN.style(text) +fun bgWhite(text: String) = BG_WHITE.style(text) +fun bgBrightBlack(text: String) = BG_BRIGHT_BLACK.style(text) +fun bgBrightRed(text: String) = BG_BRIGHT_RED.style(text) +fun bgBrightGreen(text: String) = BG_BRIGHT_GREEN.style(text) +fun bgBrightYellow(text: String) = BG_BRIGHT_YELLOW.style(text) +fun bgBrightBlue(text: String) = BG_BRIGHT_BLUE.style(text) +fun bgBrightMagenta(text: String) = BG_BRIGHT_MAGENTA.style(text) +fun bgBrightCyan(text: String) = BG_BRIGHT_CYAN.style(text) +fun bgBrightWhite(text: String) = BG_BRIGHT_WHITE.style(text) + +@OptIn(ExperimentalUnsignedTypes::class) +fun rgb8(color: UByte) = Code(39U, 38U, 5U, color) + +fun rgb8(str: String, color: UByte) = rgb8(color).style(str) + +@OptIn(ExperimentalUnsignedTypes::class) +fun bgRgb8(color: UByte) = Code(49U, 48U, 5U, color) + +fun bgRgb8(str: String, color: UByte) = bgRgb8(color).style(str) + +@OptIn(ExperimentalUnsignedTypes::class) +fun rgb24(r: UByte, g: UByte, b: UByte) = Code(39U, 38U, 2U, r, g, b) + +fun rgb24(str: String, r: UByte, g: UByte, b: UByte) = rgb24(r, g, b).style(str) + +fun rgb24(color: UInt) = rgb24( + ((color shr 16) and 0xFFU).toUByte(), + ((color shr 8) and 0xFFU).toUByte(), + (color and 0xFFU).toUByte(), +) + +fun rgb24(str: String, color: UInt) = rgb24(color).style(str) + +@OptIn(ExperimentalUnsignedTypes::class) +fun bgRgb24(r: UByte, g: UByte, b: UByte) = Code(49U, 48U, 2U, r, g, b) + +fun bgRgb24(str: String, r: UByte, g: UByte, b: UByte) = bgRgb24(r, g, b).style(str) + +fun bgRgb24(color: UInt) = bgRgb24( + ((color shr 16) and 0xFFU).toUByte(), + ((color shr 8) and 0xFFU).toUByte(), + (color and 0xFFU).toUByte(), +) + +fun bgRgb24(str: String, color: UInt) = bgRgb24(color).style(str) diff --git a/projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Stylus.kt b/projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Stylus.kt new file mode 100644 index 0000000..050755e --- /dev/null +++ b/projects/logging/colorful/src/main/kotlin/io/tnboot/logging/util/colorful/Stylus.kt @@ -0,0 +1,14 @@ +package io.tnboot.logging.util.colorful + +/** + * Container for a ANSI color codes. + */ +interface Stylus { + /** + * Apply the style to the string. + * + * @param str The string to apply the style to. + * @return The string with the style applied. + */ + fun style(str: String): String +} diff --git a/projects/logging/colorful/src/test/kotlin/io/tnboot/logging/util/colorful/ModTests.kt b/projects/logging/colorful/src/test/kotlin/io/tnboot/logging/util/colorful/ModTests.kt new file mode 100644 index 0000000..859b764 --- /dev/null +++ b/projects/logging/colorful/src/test/kotlin/io/tnboot/logging/util/colorful/ModTests.kt @@ -0,0 +1,309 @@ +package io.tnboot.logging.util.colorful + +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ModTests { + @BeforeTest + fun setup() = setEnabledState(true) + + @AfterTest + fun teardown() = setEnabledState(false) + + @Test + fun `reset()`() { + assertEquals("\u001b[0mfoo bar\u001b[0m", reset("foo bar")) + } + + @Test + fun `red() single color`() { + assertEquals("\u001b[31mfoo bar\u001b[39m", red("foo bar")) + } + + @Test + fun `bgBlue() red() double color`() { + assertEquals("\u001b[44m\u001b[31mfoo bar\u001b[39m\u001b[49m", bgBlue(red("foo bar"))) + } + + @Test + fun `red() replaces close characters`() { + assertEquals("\u001b[31mHel\u001b[31mlo\u001b[39m", red("Hel\u001b[39mlo")) + } + + @Test + fun `isEnabled handles enabled colors`() { + assertTrue(isEnabled) + setEnabledState(false) + assertFalse(isEnabled) + assertEquals("foo bar", red("foo bar")) + setEnabledState(true) + assertTrue(isEnabled) + assertEquals("\u001b[31mfoo bar\u001b[39m", red("foo bar")) + disable() + assertFalse(isEnabled) + assertEquals("foo bar", red("foo bar")) + enable() + assertTrue(isEnabled) + assertEquals("\u001b[31mfoo bar\u001b[39m", red("foo bar")) + toggleEnabledState() + assertFalse(isEnabled) + assertEquals("foo bar", red("foo bar")) + toggleEnabledState() + assertTrue(isEnabled) + assertEquals("\u001b[31mfoo bar\u001b[39m", red("foo bar")) + } + + @Test + fun `bold()`() { + assertEquals("\u001b[1mfoo bar\u001b[22m", bold("foo bar")) + } + + @Test + fun `dim()`() { + assertEquals("\u001b[2mfoo bar\u001b[22m", dim("foo bar")) + } + + @Test + fun `italic()`() { + assertEquals("\u001b[3mfoo bar\u001b[23m", italic("foo bar")) + } + + @Test + fun `underline()`() { + assertEquals("\u001b[4mfoo bar\u001b[24m", underline("foo bar")) + } + + @Test + fun `inverse()`() { + assertEquals("\u001b[7mfoo bar\u001b[27m", inverse("foo bar")) + } + + @Test + fun `hidden()`() { + assertEquals("\u001b[8mfoo bar\u001b[28m", hidden("foo bar")) + } + + @Test + fun `strikeThrough()`() { + assertEquals("\u001b[9mfoo bar\u001b[29m", strikeThrough("foo bar")) + } + + @Test + fun `black()`() { + assertEquals("\u001b[30mfoo bar\u001b[39m", black("foo bar")) + } + + @Test + fun `red()`() { + assertEquals("\u001b[31mfoo bar\u001b[39m", red("foo bar")) + } + + @Test + fun `green()`() { + assertEquals("\u001b[32mfoo bar\u001b[39m", green("foo bar")) + } + + @Test + fun `yellow()`() { + assertEquals("\u001b[33mfoo bar\u001b[39m", yellow("foo bar")) + } + + @Test + fun `blue()`() { + assertEquals("\u001b[34mfoo bar\u001b[39m", blue("foo bar")) + } + + @Test + fun `magenta()`() { + assertEquals("\u001b[35mfoo bar\u001b[39m", magenta("foo bar")) + } + + @Test + fun `cyan()`() { + assertEquals("\u001b[36mfoo bar\u001b[39m", cyan("foo bar")) + } + + @Test + fun `white()`() { + assertEquals("\u001b[37mfoo bar\u001b[39m", white("foo bar")) + } + + @Test + fun `brightBlack()`() { + assertEquals("\u001b[90mfoo bar\u001b[39m", brightBlack("foo bar")) + } + + @Test + fun `brightRed()`() { + assertEquals("\u001b[91mfoo bar\u001b[39m", brightRed("foo bar")) + } + + @Test + fun `brightGreen()`() { + assertEquals("\u001b[92mfoo bar\u001b[39m", brightGreen("foo bar")) + } + + @Test + fun `brightYellow()`() { + assertEquals("\u001b[93mfoo bar\u001b[39m", brightYellow("foo bar")) + } + + @Test + fun `brightBlue()`() { + assertEquals("\u001b[94mfoo bar\u001b[39m", brightBlue("foo bar")) + } + + @Test + fun `brightMagenta()`() { + assertEquals("\u001b[95mfoo bar\u001b[39m", brightMagenta("foo bar")) + } + + @Test + fun `brightCyan()`() { + assertEquals("\u001b[96mfoo bar\u001b[39m", brightCyan("foo bar")) + } + + @Test + fun `brightWhite()`() { + assertEquals("\u001b[97mfoo bar\u001b[39m", brightWhite("foo bar")) + } + + @Test + fun `bgBlack()`() { + assertEquals("\u001b[40mfoo bar\u001b[49m", bgBlack("foo bar")) + } + + @Test + fun `bgRed()`() { + assertEquals("\u001b[41mfoo bar\u001b[49m", bgRed("foo bar")) + } + + @Test + fun `bgGreen()`() { + assertEquals("\u001b[42mfoo bar\u001b[49m", bgGreen("foo bar")) + } + + @Test + fun `bgYellow()`() { + assertEquals("\u001b[43mfoo bar\u001b[49m", bgYellow("foo bar")) + } + + @Test + fun `bgBlue()`() { + assertEquals("\u001b[44mfoo bar\u001b[49m", bgBlue("foo bar")) + } + + @Test + fun `bgMagenta()`() { + assertEquals("\u001b[45mfoo bar\u001b[49m", bgMagenta("foo bar")) + } + + @Test + fun `bgCyan()`() { + assertEquals("\u001b[46mfoo bar\u001b[49m", bgCyan("foo bar")) + } + + @Test + fun `bgWhite()`() { + assertEquals("\u001b[47mfoo bar\u001b[49m", bgWhite("foo bar")) + } + + @Test + fun `bgBrightBlack()`() { + assertEquals("\u001b[100mfoo bar\u001b[49m", bgBrightBlack("foo bar")) + } + + @Test + fun `bgBrightRed()`() { + assertEquals("\u001b[101mfoo bar\u001b[49m", bgBrightRed("foo bar")) + } + + @Test + fun `bgBrightGreen()`() { + assertEquals("\u001b[102mfoo bar\u001b[49m", bgBrightGreen("foo bar")) + } + + @Test + fun `bgBrightYellow()`() { + assertEquals("\u001b[103mfoo bar\u001b[49m", bgBrightYellow("foo bar")) + } + + @Test + fun `bgBrightBlue()`() { + assertEquals("\u001b[104mfoo bar\u001b[49m", bgBrightBlue("foo bar")) + } + + @Test + fun `bgBrightMagenta()`() { + assertEquals("\u001b[105mfoo bar\u001b[49m", bgBrightMagenta("foo bar")) + } + + @Test + fun `bgBrightCyan()`() { + assertEquals("\u001b[106mfoo bar\u001b[49m", bgBrightCyan("foo bar")) + } + + @Test + fun `bgBrightWhite()`() { + assertEquals("\u001b[107mfoo bar\u001b[49m", bgBrightWhite("foo bar")) + } + + @Test + fun `bgBrightWhite() with red()`() { + assertEquals("\u001b[107m\u001b[31mfoo bar\u001b[39m\u001b[49m", bgBrightWhite(red("foo bar"))) + } + + @Test + fun `bgBrightWhite() with red() and bold()`() { + assertEquals( + "\u001b[107m\u001b[1m\u001b[31mfoo bar\u001b[39m\u001b[22m\u001b[49m", + bgBrightWhite(bold(red("foo bar"))), + ) + } + + @Test + fun `rgb8()`() { + assertEquals("\u001b[38;5;42mfoo bar\u001b[39m", rgb8("foo bar", 42U)) + } + + @Test + fun `bgRgb8()`() { + assertEquals("\u001b[48;5;42mfoo bar\u001b[49m", bgRgb8("foo bar", 42U)) + } + + @Test + fun `rgb24()`() { + assertEquals( + "\u001b[38;2;41;42;43mfoo bar\u001b[39m", + rgb24("foo bar", 41U, 42U, 43U), + ) + } + + @Test + fun `bgRgb24()`() { + assertEquals( + "\u001b[48;2;41;42;43mfoo bar\u001b[49m", + bgRgb24("foo bar", 41U, 42U, 43U), + ) + } + + @Test + fun `rgb24() hex`() { + assertEquals( + "\u001b[38;2;41;42;43mfoo bar\u001b[39m", + rgb24("foo bar", 0x292A2BU), + ) + } + + @Test + fun `bgRgb24() hex`() { + assertEquals( + "\u001b[48;2;41;42;43mfoo bar\u001b[49m", + bgRgb24("foo bar", 0x292A2BU), + ) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index d67da52..c60ef13 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,3 +4,7 @@ include( ":telenor-boot-dependencies", ":gradle-plugin", ) + +include( + ":projects:logging:colorful", +)