diff --git a/Benchmarks_test.go b/Benchmarks_test.go index c811429..630f902 100644 --- a/Benchmarks_test.go +++ b/Benchmarks_test.go @@ -1,37 +1,38 @@ package color_test import ( + "io" "testing" - "git.urbach.dev/go/color" + "git.akyoto.dev/go/color" ) func BenchmarkRGB(b *testing.B) { - for b.Loop() { + for i := 0; i < b.N; i++ { color.RGB(1.0, 1.0, 1.0) } } func BenchmarkLCH(b *testing.B) { - for b.Loop() { + for i := 0; i < b.N; i++ { color.LCH(0.5, 0.5, 0.0) } } -func BenchmarkPrint(b *testing.B) { +func BenchmarkFprintColorized(b *testing.B) { color.Terminal = true c := color.RGB(1.0, 1.0, 1.0) - for b.Loop() { - c.Print("") + for i := 0; i < b.N; i++ { + c.Fprint(io.Discard, "") } } -func BenchmarkPrintRaw(b *testing.B) { +func BenchmarkFprintRaw(b *testing.B) { color.Terminal = false c := color.RGB(1.0, 1.0, 1.0) - for b.Loop() { - c.Print("") + for i := 0; i < b.N; i++ { + c.Fprint(io.Discard, "") } } diff --git a/Color.go b/Color.go index 5b007c9..3ffa1dc 100644 --- a/Color.go +++ b/Color.go @@ -2,41 +2,50 @@ package color import ( "fmt" + "io" ) -// Color represents an sRGB color. +// Value is a type definition for the data type of a single color component. +type Value = float64 + +// Color represents an RGB color. type Color struct { - R byte - G byte - B byte + R Value + G Value + B Value +} + +// RGB creates a new color with red, green and blue values in the range of 0.0 to 1.0. +func RGB(r Value, g Value, b Value) Color { + return Color{r, g, b} +} + +// Fprint writes the text in the given color to the writer. +func (c Color) Fprint(writer io.Writer, text string) { + if !Terminal { + fmt.Fprint(writer, text) + return + } + + fmt.Fprintf(writer, format, byte(c.R*255), byte(c.G*255), byte(c.B*255), text) } // Print writes the text in the given color to standard output. -func (c Color) Print(args ...any) { - if !Terminal || !TrueColor { - fmt.Print(args...) +func (c Color) Print(text string) { + if !Terminal { + fmt.Print(text) return } - fmt.Printf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", c.R, c.G, c.B, fmt.Sprint(args...)) -} - -// Printf formats according to a format specifier and writes the text in the given color to standard output. -func (c Color) Printf(format string, args ...any) { - if !Terminal || !TrueColor { - fmt.Printf(format, args...) - return - } - - fmt.Printf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", c.R, c.G, c.B, fmt.Sprintf(format, args...)) + fmt.Printf(format, byte(c.R*255), byte(c.G*255), byte(c.B*255), text) } // Println writes the text in the given color to standard output and appends a newline. -func (c Color) Println(args ...any) { - if !Terminal || !TrueColor { - fmt.Println(args...) +func (c Color) Println(text string) { + if !Terminal { + fmt.Println(text) return } - fmt.Printf("\x1b[38;2;%d;%d;%dm%s\n\x1b[0m", c.R, c.G, c.B, fmt.Sprint(args...)) + fmt.Printf(formatLine, byte(c.R*255), byte(c.G*255), byte(c.B*255), text) } diff --git a/Color_test.go b/Color_test.go index 1abfa15..87ad195 100644 --- a/Color_test.go +++ b/Color_test.go @@ -1,55 +1,83 @@ package color_test import ( + "io" "testing" - "git.urbach.dev/go/color" + "git.akyoto.dev/go/assert" + "git.akyoto.dev/go/color" ) +func TestFprint(t *testing.T) { + color.Terminal = true + + color.RGB(1, 0, 0).Fprint(io.Discard, "red") + color.RGB(0, 1, 0).Fprint(io.Discard, "green") + color.RGB(0, 0, 1).Fprint(io.Discard, "blue") + + color.Terminal = false + + color.RGB(1, 0, 0).Fprint(io.Discard, "red") + color.RGB(0, 1, 0).Fprint(io.Discard, "green") + color.RGB(0, 0, 1).Fprint(io.Discard, "blue") +} + func TestPrint(t *testing.T) { color.Terminal = true - color.TrueColor = true color.RGB(1, 0, 0).Print("red\n") color.RGB(0, 1, 0).Print("green\n") color.RGB(0, 0, 1).Print("blue\n") color.Terminal = false - color.TrueColor = false color.RGB(1, 0, 0).Print("red\n") color.RGB(0, 1, 0).Print("green\n") color.RGB(0, 0, 1).Print("blue\n") } -func TestPrintf(t *testing.T) { - color.Terminal = true - color.TrueColor = true - - color.RGB(1, 0, 0).Printf("%s\n", "red") - color.RGB(0, 1, 0).Printf("%s\n", "green") - color.RGB(0, 0, 1).Printf("%s\n", "blue") - - color.Terminal = false - color.TrueColor = false - - color.RGB(1, 0, 0).Printf("%s\n", "red") - color.RGB(0, 1, 0).Printf("%s\n", "green") - color.RGB(0, 0, 1).Printf("%s\n", "blue") -} - func TestPrintln(t *testing.T) { color.Terminal = true - color.TrueColor = true color.RGB(1, 0, 0).Println("red") color.RGB(0, 1, 0).Println("green") color.RGB(0, 0, 1).Println("blue") color.Terminal = false - color.TrueColor = false color.RGB(1, 0, 0).Println("red") color.RGB(0, 1, 0).Println("green") color.RGB(0, 0, 1).Println("blue") } + +func TestRGB(t *testing.T) { + color.Terminal = true + + rgbColors := map[string]color.Color{ + "black": color.RGB(0, 0, 0), + "white": color.RGB(1, 1, 1), + "gray": color.RGB(0.5, 0.5, 0.5), + "red": color.RGB(1, 0, 0), + "green": color.RGB(0, 1, 0), + "blue": color.RGB(0, 0, 1), + "cyan": color.RGB(0, 1, 1), + "yellow": color.RGB(1, 1, 0), + "orange": color.RGB(1, 0.5, 0), + "magenta": color.RGB(1, 0, 1), + } + + for name, c := range rgbColors { + testColorRange(t, c) + c.Println("█ " + name) + } +} + +func testColorRange(t *testing.T, c color.Color) { + assert.True(t, c.R >= 0.0) + assert.True(t, c.G >= 0.0) + assert.True(t, c.B >= 0.0) + + assert.True(t, c.R <= 1.0) + assert.True(t, c.G <= 1.0) + assert.True(t, c.B <= 1.0) +} diff --git a/HSL.go b/HSL.go deleted file mode 100644 index f5cda9a..0000000 --- a/HSL.go +++ /dev/null @@ -1,29 +0,0 @@ -package color - -import "math" - -// HSL represents a color using hue, saturation and lightness. -func HSL(hue Value, saturation Value, lightness Value) Color { - hue = math.Mod(hue, 360) - c := (1 - math.Abs(2*lightness-1)) * saturation - x := c * (1 - math.Abs(math.Mod(hue/60, 2)-1)) - var r, g, b Value - - switch { - case hue >= 0 && hue < 60: - r, g, b = c, x, 0 - case hue >= 60 && hue < 120: - r, g, b = x, c, 0 - case hue >= 120 && hue < 180: - r, g, b = 0, c, x - case hue >= 180 && hue < 240: - r, g, b = 0, x, c - case hue >= 240 && hue < 300: - r, g, b = x, 0, c - case hue >= 300 && hue < 360: - r, g, b = c, 0, x - } - - m := lightness - c/2 - return RGB(r+m, g+m, b+m) -} diff --git a/HSL_test.go b/HSL_test.go deleted file mode 100644 index 0c3e654..0000000 --- a/HSL_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package color_test - -import ( - "fmt" - "testing" - - "git.urbach.dev/go/color" -) - -func TestHSLSpectrum(t *testing.T) { - color.Terminal = true - color.TrueColor = true - - for lightness := range 21 { - for hue := range 80 { - h := color.Value(hue) * 4.4 - s := color.Value(1.0) - l := color.Value(lightness) * 0.05 - - c := color.HSL(h, s, l) - c.Print("█") - } - - fmt.Println() - } -} diff --git a/HSV.go b/HSV.go deleted file mode 100644 index 2efa5fd..0000000 --- a/HSV.go +++ /dev/null @@ -1,29 +0,0 @@ -package color - -import "math" - -// HSV represents a color using hue, saturation and value. -func HSV(hue Value, saturation Value, value Value) Color { - hue = math.Mod(hue, 360) - c := value * saturation - x := c * (1 - math.Abs(math.Mod(hue/60, 2)-1)) - var r, g, b Value - - switch { - case hue >= 0 && hue < 60: - r, g, b = c, x, 0 - case hue >= 60 && hue < 120: - r, g, b = x, c, 0 - case hue >= 120 && hue < 180: - r, g, b = 0, c, x - case hue >= 180 && hue < 240: - r, g, b = 0, x, c - case hue >= 240 && hue < 300: - r, g, b = x, 0, c - case hue >= 300 && hue < 360: - r, g, b = c, 0, x - } - - m := value - c - return RGB(r+m, g+m, b+m) -} diff --git a/HSV_test.go b/HSV_test.go deleted file mode 100644 index c2b4bd3..0000000 --- a/HSV_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package color_test - -import ( - "fmt" - "testing" - - "git.urbach.dev/go/color" -) - -func TestHSVSpectrum(t *testing.T) { - color.Terminal = true - color.TrueColor = true - - for value := range 21 { - for hue := range 80 { - h := color.Value(hue) * 4.4 - s := color.Value(1.0) - v := color.Value(value) * 0.05 - - c := color.HSV(h, s, v) - c.Print("█") - } - - fmt.Println() - } -} diff --git a/LCH.go b/LCH.go index c27d4df..fb2b662 100644 --- a/LCH.go +++ b/LCH.go @@ -19,7 +19,12 @@ func LCH(lightness Value, chroma Value, hue Value) Color { b *= chroma r, g, b := oklabToLinearRGB(lightness, a, b) - return RGB(r, g, b) + + r = sRGB(r) + g = sRGB(g) + b = sRGB(b) + + return Color{r, g, b} } // findChromaInSRGB tries to find the closest chroma that can be represented in sRGB color space. diff --git a/LCH_test.go b/LCH_test.go index 59e9097..6dc33a2 100644 --- a/LCH_test.go +++ b/LCH_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "git.urbach.dev/go/color" + "git.akyoto.dev/go/color" ) func TestLCH(t *testing.T) { @@ -18,29 +18,27 @@ func TestLCH(t *testing.T) { "red": color.LCH(0.75, 1.0, 40), "orange": color.LCH(0.75, 1.0, 60), "yellow": color.LCH(0.9, 1.0, 100), - "green": color.LCH(0.75, 1.0, 135), + "green": color.LCH(0.75, 1.0, 150), "blue": color.LCH(0.75, 1.0, 260), "cyan": color.LCH(0.75, 1.0, 210), "magenta": color.LCH(0.75, 1.0, 320), } for name, c := range lchColors { + testColorRange(t, c) c.Println("█ " + name) } } func TestLCHSpectrum(t *testing.T) { color.Terminal = true - color.TrueColor = true for chroma := range 4 { for lightness := range 21 { for hue := range 80 { - l := color.Value(lightness) * 0.05 - c := color.Value(chroma) * 0.05 - h := color.Value(hue) * 4.4 - col := color.LCH(l, c, h) - col.Print("█") + c := color.LCH(color.Value(lightness)*0.05, color.Value(chroma)*0.05, color.Value(hue)*4.4) + testColorRange(t, c) + c.Print("█") } fmt.Println() diff --git a/README.md b/README.md index 56fe49a..e7b485b 100644 --- a/README.md +++ b/README.md @@ -4,60 +4,52 @@ Adds color to your terminal output. ## Features -- ANSI colors -- HSL colors -- HSV colors -- LCH colors -- RGB colors +- RGB color space +- LCH color space (oklch) +- Truecolor terminal output +- Zero dependencies (excluding tests) ## Installation ```shell -go get git.urbach.dev/go/color +go get git.akyoto.dev/go/color ``` ## Usage ```go -// ANSI -ansi.Red.Println("red text") +red := color.RGB(1.0, 0.0, 0.0) +red.Println("red text") -// LCH -green := color.LCH(0.5, 1.0, 135) -green.Println("green text") - -// RGB -blue := color.RGB(0, 0, 1) -blue.Println("blue text") +orange := color.LCH(0.7, 1.0, 65) +orange.Println("orange text") ``` ## Tests ``` +PASS: TestFprint PASS: TestPrint -PASS: TestPrintf PASS: TestPrintln -PASS: TestHSLSpectrum -PASS: TestHSVSpectrum +PASS: TestRGB PASS: TestLCH PASS: TestLCHSpectrum -PASS: TestRGB coverage: 100.0% of statements ``` ## Benchmarks ``` -BenchmarkRGB-20 100000000 14.88 ns/op 0 B/op 0 allocs/op -BenchmarkLCH-20 5075756 227.8 ns/op 0 B/op 0 allocs/op -BenchmarkPrint-20 1587134 755.5 ns/op 0 B/op 0 allocs/op -BenchmarkPrintRaw-20 3166090 361.5 ns/op 0 B/op 0 allocs/op +BenchmarkRGB-12 1000000000 0.3132 ns/op 0 B/op 0 allocs/op +BenchmarkLCH-12 4802006 249.8 ns/op 0 B/op 0 allocs/op +BenchmarkFprintColorized-12 6356535 188.4 ns/op 0 B/op 0 allocs/op +BenchmarkFprintRaw-12 27374659 43.76 ns/op 0 B/op 0 allocs/op ``` ## License -Please see the [license documentation](https://urbach.dev/license). +Please see the [license documentation](https://akyoto.dev/license). ## Copyright -© 2024 Eduard Urbach \ No newline at end of file +© 2024 Eduard Urbach diff --git a/RGB_test.go b/RGB_test.go deleted file mode 100644 index a1f4481..0000000 --- a/RGB_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package color_test - -import ( - "testing" - - "git.urbach.dev/go/color" -) - -func TestRGB(t *testing.T) { - color.Terminal = true - color.TrueColor = true - - rgbColors := map[string]color.Color{ - "black": color.RGB(0, 0, 0), - "white": color.RGB(1, 1, 1), - "gray": color.RGB(0.5, 0.5, 0.5), - "red": color.RGB(1, 0, 0), - "green": color.RGB(0, 1, 0), - "blue": color.RGB(0, 0, 1), - "cyan": color.RGB(0, 1, 1), - "yellow": color.RGB(1, 1, 0), - "orange": color.RGB(1, 0.5, 0), - "magenta": color.RGB(1, 0, 1), - } - - for name, c := range rgbColors { - c.Println("█ " + name) - } -} diff --git a/Terminal.go b/Terminal.go index e47cf17..e2a73d8 100644 --- a/Terminal.go +++ b/Terminal.go @@ -2,12 +2,13 @@ package color import ( "os" - - "git.urbach.dev/go/color/tty" ) -// Terminal indicates if we're in a terminal with truecolor support. -var Terminal = tty.IsTerminal(os.Stdout.Fd()) +// These constants represent the escape codes needed to display color in terminals. +const ( + format = "\x1b[38;2;%d;%d;%dm%s\x1b[0m" + formatLine = "\x1b[38;2;%d;%d;%dm%s\n\x1b[0m" +) -// TrueColor indicates if the terminal has 24-bit color support. -var TrueColor = os.Getenv("COLORTERM") == "truecolor" +// Terminal is a boolean that indicates if we're in a terminal with truecolor support. +var Terminal = os.Getenv("COLORTERM") == "truecolor" diff --git a/Value.go b/Value.go deleted file mode 100644 index 42635f1..0000000 --- a/Value.go +++ /dev/null @@ -1,4 +0,0 @@ -package color - -// Value is a type definition for the data type of a single color component. -type Value = float64 diff --git a/ansi/Code.go b/ansi/Code.go deleted file mode 100644 index 46c68bd..0000000 --- a/ansi/Code.go +++ /dev/null @@ -1,40 +0,0 @@ -package ansi - -import ( - "fmt" - - "git.urbach.dev/go/color" -) - -// Code represents an ANSI escape code. -type Code int - -// Print writes the text in the given color to standard output. -func (code Code) Print(args ...any) { - if !color.Terminal { - fmt.Print(args...) - return - } - - fmt.Printf("\x1b[%dm%s\x1b[0m", code, fmt.Sprint(args...)) -} - -// Printf formats according to a format specifier and writes the text in the given color to standard output. -func (code Code) Printf(format string, args ...any) { - if !color.Terminal { - fmt.Printf(format, args...) - return - } - - fmt.Printf("\x1b[%dm%s\x1b[0m", code, fmt.Sprintf(format, args...)) -} - -// Println writes the text in the given color to standard output and appends a newline. -func (code Code) Println(args ...any) { - if !color.Terminal { - fmt.Println(args...) - return - } - - fmt.Printf("\x1b[%dm%s\n\x1b[0m", code, fmt.Sprint(args...)) -} diff --git a/ansi/Code_test.go b/ansi/Code_test.go deleted file mode 100644 index 64a1f47..0000000 --- a/ansi/Code_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package ansi_test - -import ( - "testing" - - "git.urbach.dev/go/color" - "git.urbach.dev/go/color/ansi" -) - -func TestPrintRaw(t *testing.T) { - color.Terminal = false - testPrint() - testPrintf() - testPrintln() -} - -func TestPrint(t *testing.T) { - color.Terminal = true - testPrint() - testPrintf() - testPrintln() -} - -func testPrint() { - ansi.Black.Print("Black\n") - ansi.White.Print("White\n") - ansi.Red.Print("Red\n") - ansi.Green.Print("Green\n") - ansi.Blue.Print("Blue\n") - ansi.Yellow.Print("Yellow\n") - ansi.Magenta.Print("Magenta\n") - ansi.Cyan.Print("Cyan\n") -} - -func testPrintf() { - ansi.Black.Printf("%s\n", "Black") - ansi.White.Printf("%s\n", "White") - ansi.Red.Printf("%s\n", "Red") - ansi.Green.Printf("%s\n", "Green") - ansi.Blue.Printf("%s\n", "Blue") - ansi.Yellow.Printf("%s\n", "Yellow") - ansi.Magenta.Printf("%s\n", "Magenta") - ansi.Cyan.Printf("%s\n", "Cyan") -} - -func testPrintln() { - ansi.Black.Println("Black") - ansi.White.Println("White") - ansi.Red.Println("Red") - ansi.Green.Println("Green") - ansi.Blue.Println("Blue") - ansi.Yellow.Println("Yellow") - ansi.Magenta.Println("Magenta") - ansi.Cyan.Println("Cyan") -} diff --git a/ansi/Color.go b/ansi/Color.go deleted file mode 100644 index 65bacda..0000000 --- a/ansi/Color.go +++ /dev/null @@ -1,12 +0,0 @@ -package ansi - -const ( - Black Code = iota + 30 - Red - Green - Yellow - Blue - Magenta - Cyan - White -) diff --git a/ansi/Format.go b/ansi/Format.go deleted file mode 100644 index 29bfcf6..0000000 --- a/ansi/Format.go +++ /dev/null @@ -1,14 +0,0 @@ -package ansi - -const ( - Reset Code = iota - Bold - Dim - Italic - Underline - Blink - BlinkFast - Reverse - Hidden - Strikethrough -) diff --git a/go.mod b/go.mod index c3152ec..481b841 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ -module git.urbach.dev/go/color +module git.akyoto.dev/go/color -go 1.24 +go 1.22.0 -require golang.org/x/sys v0.31.0 +require git.akyoto.dev/go/assert v0.1.3 diff --git a/go.sum b/go.sum index c55261f..9fc2547 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +git.akyoto.dev/go/assert v0.1.3 h1:QwCUbmG4aZYsNk/OuRBz1zWVKmGlDUHhOnnDBfn8Qw8= +git.akyoto.dev/go/assert v0.1.3/go.mod h1:0GzMaM0eURuDwtGkJJkCsI7r2aUKr+5GmWNTFPgDocM= diff --git a/RGB.go b/sRGB.go similarity index 76% rename from RGB.go rename to sRGB.go index 3d94abc..1d522c8 100644 --- a/RGB.go +++ b/sRGB.go @@ -2,15 +2,6 @@ package color import "math" -// RGB creates a new sRGB color. -func RGB(r Value, g Value, b Value) Color { - return Color{ - byte(sRGB(r) * 255), - byte(sRGB(g) * 255), - byte(sRGB(b) * 255), - } -} - // inSRGB indicates whether the given color can be mapped to the sRGB color space. func inSRGB(l Value, a Value, b Value, chroma Value) bool { r, g, b := oklabToLinearRGB(l, a*chroma, b*chroma) diff --git a/tty/Terminal_bsd.go b/tty/Terminal_bsd.go deleted file mode 100644 index 15e06e0..0000000 --- a/tty/Terminal_bsd.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build darwin || freebsd - -package tty - -import "golang.org/x/sys/unix" - -// IsTerminal returns true if the file descriptor is a terminal. -func IsTerminal(fd uintptr) bool { - _, err := unix.IoctlGetTermios(int(fd), unix.TIOCGETA) - return err == nil -} diff --git a/tty/Terminal_linux.go b/tty/Terminal_linux.go deleted file mode 100644 index 023c564..0000000 --- a/tty/Terminal_linux.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build linux - -package tty - -import "golang.org/x/sys/unix" - -// IsTerminal returns true if the file descriptor is a terminal. -func IsTerminal(fd uintptr) bool { - _, err := unix.IoctlGetTermios(int(fd), unix.TCGETS) - return err == nil -} diff --git a/tty/Terminal_other.go b/tty/Terminal_other.go deleted file mode 100644 index 3ea5ee8..0000000 --- a/tty/Terminal_other.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build !darwin && !freebsd && !linux && !windows - -package tty - -// IsTerminal is always false on unsupported platforms. -func IsTerminal(fd uintptr) bool { - return false -} diff --git a/tty/Terminal_test.go b/tty/Terminal_test.go deleted file mode 100644 index 1f1a097..0000000 --- a/tty/Terminal_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package tty_test - -import ( - "os" - "testing" - - "git.urbach.dev/go/color/tty" -) - -func TestIsTerminal(t *testing.T) { - tty.IsTerminal(os.Stdout.Fd()) -} diff --git a/tty/Terminal_windows.go b/tty/Terminal_windows.go deleted file mode 100644 index 0bf1927..0000000 --- a/tty/Terminal_windows.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build windows - -package tty - -import "golang.org/x/sys/windows" - -// IsTerminal returns true if the file descriptor is a terminal. -func IsTerminal(fd uintptr) bool { - var mode uint32 - err := windows.GetConsoleMode(windows.Handle(fd), &mode) - return err == nil -}