From 6db2852d9e208c9a8075c5bb6b003eb3691459a3 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 11 Mar 2024 22:08:28 +0100 Subject: [PATCH 01/10] Improved interface --- Benchmarks_test.go | 20 +++++++++++++++++++- Color.go | 28 +++++++++++++++++++--------- Color_test.go | 14 ++++++++++++++ README.md | 10 ++++++---- Terminal.go | 6 ------ 5 files changed, 58 insertions(+), 20 deletions(-) diff --git a/Benchmarks_test.go b/Benchmarks_test.go index 630f902..27403b3 100644 --- a/Benchmarks_test.go +++ b/Benchmarks_test.go @@ -19,7 +19,7 @@ func BenchmarkLCH(b *testing.B) { } } -func BenchmarkFprintColorized(b *testing.B) { +func BenchmarkFprint(b *testing.B) { color.Terminal = true c := color.RGB(1.0, 1.0, 1.0) @@ -36,3 +36,21 @@ func BenchmarkFprintRaw(b *testing.B) { c.Fprint(io.Discard, "") } } + +func BenchmarkPrint(b *testing.B) { + color.Terminal = true + c := color.RGB(1.0, 1.0, 1.0) + + for i := 0; i < b.N; i++ { + c.Print("") + } +} + +func BenchmarkPrintRaw(b *testing.B) { + color.Terminal = false + c := color.RGB(1.0, 1.0, 1.0) + + for i := 0; i < b.N; i++ { + c.Print("") + } +} diff --git a/Color.go b/Color.go index 3ffa1dc..cf80739 100644 --- a/Color.go +++ b/Color.go @@ -21,31 +21,41 @@ func RGB(r Value, g Value, b Value) Color { } // Fprint writes the text in the given color to the writer. -func (c Color) Fprint(writer io.Writer, text string) { +func (c Color) Fprint(writer io.Writer, args ...any) { if !Terminal { - fmt.Fprint(writer, text) + fmt.Fprint(writer, args...) return } - fmt.Fprintf(writer, format, byte(c.R*255), byte(c.G*255), byte(c.B*255), text) + fmt.Fprintf(writer, "\x1b[38;2;%d;%d;%dm%s\x1b[0m", byte(c.R*255), byte(c.G*255), byte(c.B*255), fmt.Sprint(args...)) } // Print writes the text in the given color to standard output. -func (c Color) Print(text string) { +func (c Color) Print(args ...any) { if !Terminal { - fmt.Print(text) + fmt.Print(args...) return } - fmt.Printf(format, byte(c.R*255), byte(c.G*255), byte(c.B*255), text) + fmt.Printf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", byte(c.R*255), byte(c.G*255), byte(c.B*255), fmt.Sprint(args...)) +} + +// Print writes the text in the given color to standard output. +func (c Color) Printf(format string, args ...any) { + if !Terminal { + fmt.Printf(format, args...) + return + } + + fmt.Printf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", byte(c.R*255), byte(c.G*255), byte(c.B*255), fmt.Sprintf(format, args...)) } // Println writes the text in the given color to standard output and appends a newline. -func (c Color) Println(text string) { +func (c Color) Println(args ...any) { if !Terminal { - fmt.Println(text) + fmt.Println(args...) return } - fmt.Printf(formatLine, byte(c.R*255), byte(c.G*255), byte(c.B*255), text) + fmt.Printf("\x1b[38;2;%d;%d;%dm%s\n\x1b[0m", byte(c.R*255), byte(c.G*255), byte(c.B*255), fmt.Sprint(args...)) } diff --git a/Color_test.go b/Color_test.go index 87ad195..45646d7 100644 --- a/Color_test.go +++ b/Color_test.go @@ -36,6 +36,20 @@ func TestPrint(t *testing.T) { color.RGB(0, 0, 1).Print("blue\n") } +func TestPrintf(t *testing.T) { + color.Terminal = 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.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 diff --git a/README.md b/README.md index e7b485b..fe0580d 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,12 @@ coverage: 100.0% of statements ## Benchmarks ``` -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 +BenchmarkRGB-12 1000000000 0.3211 ns/op 0 B/op 0 allocs/op +BenchmarkLCH-12 4767306 251.8 ns/op 0 B/op 0 allocs/op +BenchmarkFprint-12 4869368 245.7 ns/op 0 B/op 0 allocs/op +BenchmarkFprintRaw-12 23155356 43.73 ns/op 0 B/op 0 allocs/op +BenchmarkPrint-12 358099 3560 ns/op 0 B/op 0 allocs/op +BenchmarkPrintRaw-12 3144412 378.9 ns/op 0 B/op 0 allocs/op ``` ## License diff --git a/Terminal.go b/Terminal.go index e2a73d8..d6ab79f 100644 --- a/Terminal.go +++ b/Terminal.go @@ -4,11 +4,5 @@ import ( "os" ) -// 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" -) - // Terminal is a boolean that indicates if we're in a terminal with truecolor support. var Terminal = os.Getenv("COLORTERM") == "truecolor" From 55da31ffd6cafe5bc296169d50b0131a211ee742 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 11 Mar 2024 22:09:45 +0100 Subject: [PATCH 02/10] Added missing test case --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fe0580d..6a7ce08 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ orange.Println("orange text") ``` PASS: TestFprint PASS: TestPrint +PASS: TestPrintf PASS: TestPrintln PASS: TestRGB PASS: TestLCH From 929938dcd667b31fd505f9d6d2ed2aae85844724 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 11 Mar 2024 23:28:37 +0100 Subject: [PATCH 03/10] Added ANSI colors --- Color.go | 2 +- ansi/Code.go | 51 ++++++++++++++++++++++++++++++++++++++++++ ansi/Code_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++++++ ansi/Color.go | 12 ++++++++++ ansi/Format.go | 14 ++++++++++++ 5 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 ansi/Code.go create mode 100644 ansi/Code_test.go create mode 100644 ansi/Color.go create mode 100644 ansi/Format.go diff --git a/Color.go b/Color.go index cf80739..cff542b 100644 --- a/Color.go +++ b/Color.go @@ -40,7 +40,7 @@ func (c Color) Print(args ...any) { fmt.Printf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", byte(c.R*255), byte(c.G*255), byte(c.B*255), fmt.Sprint(args...)) } -// Print writes the text in the given color to standard output. +// 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 { fmt.Printf(format, args...) diff --git a/ansi/Code.go b/ansi/Code.go new file mode 100644 index 0000000..037dd8d --- /dev/null +++ b/ansi/Code.go @@ -0,0 +1,51 @@ +package ansi + +import ( + "fmt" + "io" + + "git.akyoto.dev/go/color" +) + +// Code represents an ANSI escape code. +type Code int + +// Fprint writes the text in the given color to the writer. +func (code Code) Fprint(writer io.Writer, args ...any) { + if !color.Terminal { + fmt.Fprint(writer, args...) + return + } + + fmt.Fprintf(writer, "\x1b[%dm%s\x1b[0m", code, fmt.Sprint(args...)) +} + +// 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 new file mode 100644 index 0000000..6f2455e --- /dev/null +++ b/ansi/Code_test.go @@ -0,0 +1,57 @@ +package ansi_test + +import ( + "os" + "testing" + + "git.akyoto.dev/go/color" + "git.akyoto.dev/go/color/ansi" +) + +func Test(t *testing.T) { + color.Terminal = true + testColors() +} + +func TestPrintRaw(t *testing.T) { + color.Terminal = false + testColors() +} + +func testColors() { + ansi.Black.Fprint(os.Stdout, "Black\n") + ansi.White.Fprint(os.Stdout, "White\n") + ansi.Red.Fprint(os.Stdout, "Red\n") + ansi.Green.Fprint(os.Stdout, "Green\n") + ansi.Blue.Fprint(os.Stdout, "Blue\n") + ansi.Yellow.Fprint(os.Stdout, "Yellow\n") + ansi.Magenta.Fprint(os.Stdout, "Magenta\n") + ansi.Cyan.Fprint(os.Stdout, "Cyan\n") + + 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") + + 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") + + 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 new file mode 100644 index 0000000..65bacda --- /dev/null +++ b/ansi/Color.go @@ -0,0 +1,12 @@ +package ansi + +const ( + Black Code = iota + 30 + Red + Green + Yellow + Blue + Magenta + Cyan + White +) diff --git a/ansi/Format.go b/ansi/Format.go new file mode 100644 index 0000000..29bfcf6 --- /dev/null +++ b/ansi/Format.go @@ -0,0 +1,14 @@ +package ansi + +const ( + Reset Code = iota + Bold + Dim + Italic + Underline + Blink + BlinkFast + Reverse + Hidden + Strikethrough +) From 71c74c800b11d27da6f9ce0e67cb7bc62b1dbe14 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 12 Mar 2024 00:25:20 +0100 Subject: [PATCH 04/10] Added terminal detection --- Benchmarks_test.go | 19 ------------------- Color.go | 11 ----------- Color_test.go | 15 --------------- README.md | 18 +++++++----------- Terminal.go | 4 +++- ansi/Code.go | 11 ----------- ansi/Code_test.go | 10 ---------- go.mod | 5 ++++- go.sum | 2 ++ tty/Terminal_darwin.go | 6 ++++++ tty/Terminal_linux.go | 9 +++++++++ tty/Terminal_windows.go | 6 ++++++ 12 files changed, 37 insertions(+), 79 deletions(-) create mode 100644 tty/Terminal_darwin.go create mode 100644 tty/Terminal_linux.go create mode 100644 tty/Terminal_windows.go diff --git a/Benchmarks_test.go b/Benchmarks_test.go index 27403b3..012c8e0 100644 --- a/Benchmarks_test.go +++ b/Benchmarks_test.go @@ -1,7 +1,6 @@ package color_test import ( - "io" "testing" "git.akyoto.dev/go/color" @@ -19,24 +18,6 @@ func BenchmarkLCH(b *testing.B) { } } -func BenchmarkFprint(b *testing.B) { - color.Terminal = true - c := color.RGB(1.0, 1.0, 1.0) - - for i := 0; i < b.N; i++ { - c.Fprint(io.Discard, "") - } -} - -func BenchmarkFprintRaw(b *testing.B) { - color.Terminal = false - c := color.RGB(1.0, 1.0, 1.0) - - for i := 0; i < b.N; i++ { - c.Fprint(io.Discard, "") - } -} - func BenchmarkPrint(b *testing.B) { color.Terminal = true c := color.RGB(1.0, 1.0, 1.0) diff --git a/Color.go b/Color.go index cff542b..04489e9 100644 --- a/Color.go +++ b/Color.go @@ -2,7 +2,6 @@ package color import ( "fmt" - "io" ) // Value is a type definition for the data type of a single color component. @@ -20,16 +19,6 @@ 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, args ...any) { - if !Terminal { - fmt.Fprint(writer, args...) - return - } - - fmt.Fprintf(writer, "\x1b[38;2;%d;%d;%dm%s\x1b[0m", byte(c.R*255), byte(c.G*255), byte(c.B*255), fmt.Sprint(args...)) -} - // Print writes the text in the given color to standard output. func (c Color) Print(args ...any) { if !Terminal { diff --git a/Color_test.go b/Color_test.go index 45646d7..f21e033 100644 --- a/Color_test.go +++ b/Color_test.go @@ -1,27 +1,12 @@ package color_test import ( - "io" "testing" "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 diff --git a/README.md b/README.md index 6a7ce08..e7ff716 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,9 @@ Adds color to your terminal output. ## Features -- RGB color space -- LCH color space (oklch) -- Truecolor terminal output -- Zero dependencies (excluding tests) +- ANSI colors +- LCH colors +- RGB colors ## Installation @@ -28,7 +27,6 @@ orange.Println("orange text") ## Tests ``` -PASS: TestFprint PASS: TestPrint PASS: TestPrintf PASS: TestPrintln @@ -41,12 +39,10 @@ coverage: 100.0% of statements ## Benchmarks ``` -BenchmarkRGB-12 1000000000 0.3211 ns/op 0 B/op 0 allocs/op -BenchmarkLCH-12 4767306 251.8 ns/op 0 B/op 0 allocs/op -BenchmarkFprint-12 4869368 245.7 ns/op 0 B/op 0 allocs/op -BenchmarkFprintRaw-12 23155356 43.73 ns/op 0 B/op 0 allocs/op -BenchmarkPrint-12 358099 3560 ns/op 0 B/op 0 allocs/op -BenchmarkPrintRaw-12 3144412 378.9 ns/op 0 B/op 0 allocs/op +BenchmarkRGB-12 1000000000 0.3141 ns/op 0 B/op 0 allocs/op +BenchmarkLCH-12 4780176 250.9 ns/op 0 B/op 0 allocs/op +BenchmarkPrint-12 375394 3343 ns/op 0 B/op 0 allocs/op +BenchmarkPrintRaw-12 3105022 387.3 ns/op 0 B/op 0 allocs/op ``` ## License diff --git a/Terminal.go b/Terminal.go index d6ab79f..e6573d5 100644 --- a/Terminal.go +++ b/Terminal.go @@ -2,7 +2,9 @@ package color import ( "os" + + "git.akyoto.dev/go/color/tty" ) // Terminal is a boolean that indicates if we're in a terminal with truecolor support. -var Terminal = os.Getenv("COLORTERM") == "truecolor" +var Terminal = tty.IsTerminal(os.Stdout.Fd()) && os.Getenv("COLORTERM") == "truecolor" diff --git a/ansi/Code.go b/ansi/Code.go index 037dd8d..a0f8510 100644 --- a/ansi/Code.go +++ b/ansi/Code.go @@ -2,7 +2,6 @@ package ansi import ( "fmt" - "io" "git.akyoto.dev/go/color" ) @@ -10,16 +9,6 @@ import ( // Code represents an ANSI escape code. type Code int -// Fprint writes the text in the given color to the writer. -func (code Code) Fprint(writer io.Writer, args ...any) { - if !color.Terminal { - fmt.Fprint(writer, args...) - return - } - - fmt.Fprintf(writer, "\x1b[%dm%s\x1b[0m", code, fmt.Sprint(args...)) -} - // Print writes the text in the given color to standard output. func (code Code) Print(args ...any) { if !color.Terminal { diff --git a/ansi/Code_test.go b/ansi/Code_test.go index 6f2455e..a2230d6 100644 --- a/ansi/Code_test.go +++ b/ansi/Code_test.go @@ -1,7 +1,6 @@ package ansi_test import ( - "os" "testing" "git.akyoto.dev/go/color" @@ -19,15 +18,6 @@ func TestPrintRaw(t *testing.T) { } func testColors() { - ansi.Black.Fprint(os.Stdout, "Black\n") - ansi.White.Fprint(os.Stdout, "White\n") - ansi.Red.Fprint(os.Stdout, "Red\n") - ansi.Green.Fprint(os.Stdout, "Green\n") - ansi.Blue.Fprint(os.Stdout, "Blue\n") - ansi.Yellow.Fprint(os.Stdout, "Yellow\n") - ansi.Magenta.Fprint(os.Stdout, "Magenta\n") - ansi.Cyan.Fprint(os.Stdout, "Cyan\n") - ansi.Black.Print("Black\n") ansi.White.Print("White\n") ansi.Red.Print("Red\n") diff --git a/go.mod b/go.mod index 481b841..baf7b50 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module git.akyoto.dev/go/color go 1.22.0 -require git.akyoto.dev/go/assert v0.1.3 +require ( + git.akyoto.dev/go/assert v0.1.3 + golang.org/x/sys v0.18.0 +) diff --git a/go.sum b/go.sum index 9fc2547..895b93c 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ git.akyoto.dev/go/assert v0.1.3 h1:QwCUbmG4aZYsNk/OuRBz1zWVKmGlDUHhOnnDBfn8Qw8= git.akyoto.dev/go/assert v0.1.3/go.mod h1:0GzMaM0eURuDwtGkJJkCsI7r2aUKr+5GmWNTFPgDocM= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/tty/Terminal_darwin.go b/tty/Terminal_darwin.go new file mode 100644 index 0000000..48257c4 --- /dev/null +++ b/tty/Terminal_darwin.go @@ -0,0 +1,6 @@ +package tty + +// IsTerminal returns true if the file descriptor is a terminal. +func IsTerminal(fd uintptr) bool { + return false +} diff --git a/tty/Terminal_linux.go b/tty/Terminal_linux.go new file mode 100644 index 0000000..4e44a64 --- /dev/null +++ b/tty/Terminal_linux.go @@ -0,0 +1,9 @@ +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_windows.go b/tty/Terminal_windows.go new file mode 100644 index 0000000..48257c4 --- /dev/null +++ b/tty/Terminal_windows.go @@ -0,0 +1,6 @@ +package tty + +// IsTerminal returns true if the file descriptor is a terminal. +func IsTerminal(fd uintptr) bool { + return false +} From e0bfa137d5cd4b820cff6af62664bc6790ae7f97 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 12 Mar 2024 15:23:08 +0100 Subject: [PATCH 05/10] Added more tests --- Color.go | 8 -------- Color_test.go | 33 --------------------------------- LCH_test.go | 2 +- README.md | 15 ++++++++++----- sRGB.go => RGB.go | 5 +++++ RGB_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ Value.go | 4 ++++ ansi/Code_test.go | 22 +++++++++++++++------- tty/Terminal_darwin.go | 5 ++++- tty/Terminal_test.go | 12 ++++++++++++ 10 files changed, 91 insertions(+), 55 deletions(-) rename sRGB.go => RGB.go (77%) create mode 100644 RGB_test.go create mode 100644 Value.go create mode 100644 tty/Terminal_test.go diff --git a/Color.go b/Color.go index 04489e9..85be94c 100644 --- a/Color.go +++ b/Color.go @@ -4,9 +4,6 @@ import ( "fmt" ) -// 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 Value @@ -14,11 +11,6 @@ type Color struct { 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} -} - // Print writes the text in the given color to standard output. func (c Color) Print(args ...any) { if !Terminal { diff --git a/Color_test.go b/Color_test.go index f21e033..8260a09 100644 --- a/Color_test.go +++ b/Color_test.go @@ -3,7 +3,6 @@ package color_test import ( "testing" - "git.akyoto.dev/go/assert" "git.akyoto.dev/go/color" ) @@ -48,35 +47,3 @@ func TestPrintln(t *testing.T) { 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/LCH_test.go b/LCH_test.go index 6dc33a2..f2d59a9 100644 --- a/LCH_test.go +++ b/LCH_test.go @@ -18,7 +18,7 @@ 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, 150), + "green": color.LCH(0.75, 1.0, 135), "blue": color.LCH(0.75, 1.0, 260), "cyan": color.LCH(0.75, 1.0, 210), "magenta": color.LCH(0.75, 1.0, 320), diff --git a/README.md b/README.md index e7ff716..fdca232 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,16 @@ go get git.akyoto.dev/go/color ## Usage ```go -red := color.RGB(1.0, 0.0, 0.0) -red.Println("red text") +// ANSI +ansi.Red.Println("red text") -orange := color.LCH(0.7, 1.0, 65) -orange.Println("orange 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") ``` ## Tests @@ -30,9 +35,9 @@ orange.Println("orange text") PASS: TestPrint PASS: TestPrintf PASS: TestPrintln -PASS: TestRGB PASS: TestLCH PASS: TestLCHSpectrum +PASS: TestRGB coverage: 100.0% of statements ``` diff --git a/sRGB.go b/RGB.go similarity index 77% rename from sRGB.go rename to RGB.go index 1d522c8..f882f07 100644 --- a/sRGB.go +++ b/RGB.go @@ -2,6 +2,11 @@ package color import "math" +// 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} +} + // 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/RGB_test.go b/RGB_test.go new file mode 100644 index 0000000..86686e4 --- /dev/null +++ b/RGB_test.go @@ -0,0 +1,40 @@ +package color_test + +import ( + "testing" + + "git.akyoto.dev/go/assert" + "git.akyoto.dev/go/color" +) + +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/Value.go b/Value.go new file mode 100644 index 0000000..42635f1 --- /dev/null +++ b/Value.go @@ -0,0 +1,4 @@ +package color + +// Value is a type definition for the data type of a single color component. +type Value = float64 diff --git a/ansi/Code_test.go b/ansi/Code_test.go index a2230d6..a9fb83a 100644 --- a/ansi/Code_test.go +++ b/ansi/Code_test.go @@ -7,17 +7,21 @@ import ( "git.akyoto.dev/go/color/ansi" ) -func Test(t *testing.T) { - color.Terminal = true - testColors() -} - func TestPrintRaw(t *testing.T) { color.Terminal = false - testColors() + testPrint() + testPrintf() + testPrintln() } -func testColors() { +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") @@ -26,7 +30,9 @@ func testColors() { 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") @@ -35,7 +41,9 @@ func testColors() { 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") diff --git a/tty/Terminal_darwin.go b/tty/Terminal_darwin.go index 48257c4..0726279 100644 --- a/tty/Terminal_darwin.go +++ b/tty/Terminal_darwin.go @@ -1,6 +1,9 @@ package tty +import "golang.org/x/sys/unix" + // IsTerminal returns true if the file descriptor is a terminal. func IsTerminal(fd uintptr) bool { - return false + _, err := unix.IoctlGetTermios(int(fd), unix.TIOCGETA) + return err == nil } diff --git a/tty/Terminal_test.go b/tty/Terminal_test.go new file mode 100644 index 0000000..be7406f --- /dev/null +++ b/tty/Terminal_test.go @@ -0,0 +1,12 @@ +package tty_test + +import ( + "os" + "testing" + + "git.akyoto.dev/go/color/tty" +) + +func TestIsTerminal(t *testing.T) { + tty.IsTerminal(os.Stdout.Fd()) +} From 77e626de5d10b20d114b502c8c83ce5db3bfb3a2 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 21 Mar 2024 23:23:43 +0100 Subject: [PATCH 06/10] Added ANSI colors for old terminals --- Color.go | 6 +++--- Terminal.go | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Color.go b/Color.go index 85be94c..cb83a0a 100644 --- a/Color.go +++ b/Color.go @@ -13,7 +13,7 @@ type Color struct { // Print writes the text in the given color to standard output. func (c Color) Print(args ...any) { - if !Terminal { + if !Terminal || !TrueColor { fmt.Print(args...) return } @@ -23,7 +23,7 @@ func (c Color) Print(args ...any) { // 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 { + if !Terminal || !TrueColor { fmt.Printf(format, args...) return } @@ -33,7 +33,7 @@ func (c Color) Printf(format string, args ...any) { // Println writes the text in the given color to standard output and appends a newline. func (c Color) Println(args ...any) { - if !Terminal { + if !Terminal || !TrueColor { fmt.Println(args...) return } diff --git a/Terminal.go b/Terminal.go index e6573d5..d36d963 100644 --- a/Terminal.go +++ b/Terminal.go @@ -6,5 +6,8 @@ import ( "git.akyoto.dev/go/color/tty" ) -// Terminal is a boolean that indicates if we're in a terminal with truecolor support. -var Terminal = tty.IsTerminal(os.Stdout.Fd()) && os.Getenv("COLORTERM") == "truecolor" +// Terminal indicates if we're in a terminal with truecolor support. +var Terminal = tty.IsTerminal(os.Stdout.Fd()) + +// TrueColor indicates if the terminal has 24-bit color support. +var TrueColor = os.Getenv("COLORTERM") == "truecolor" From 77b109b5170886c89ee4549db4203ef5c26c6e70 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 11 Feb 2025 12:19:55 +0100 Subject: [PATCH 07/10] Added Windows support --- go.mod | 2 +- go.sum | 4 ++-- tty/Terminal_windows.go | 6 +++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index baf7b50..e0d90d7 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.22.0 require ( git.akyoto.dev/go/assert v0.1.3 - golang.org/x/sys v0.18.0 + golang.org/x/sys v0.30.0 ) diff --git a/go.sum b/go.sum index 895b93c..ed6101f 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ git.akyoto.dev/go/assert v0.1.3 h1:QwCUbmG4aZYsNk/OuRBz1zWVKmGlDUHhOnnDBfn8Qw8= git.akyoto.dev/go/assert v0.1.3/go.mod h1:0GzMaM0eURuDwtGkJJkCsI7r2aUKr+5GmWNTFPgDocM= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/tty/Terminal_windows.go b/tty/Terminal_windows.go index 48257c4..c7355d1 100644 --- a/tty/Terminal_windows.go +++ b/tty/Terminal_windows.go @@ -1,6 +1,10 @@ package tty +import "golang.org/x/sys/windows" + // IsTerminal returns true if the file descriptor is a terminal. func IsTerminal(fd uintptr) bool { - return false + var mode uint32 + err := windows.GetConsoleMode(windows.Handle(fd), &mode) + return err == nil } From cb8aacf9b85ed42f6e37397084878c8d18b8cbcb Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 16 Feb 2025 17:28:10 +0100 Subject: [PATCH 08/10] Added FreeBSD support --- tty/{Terminal_darwin.go => Terminal_bsd.go} | 2 ++ tty/Terminal_linux.go | 2 ++ tty/Terminal_other.go | 8 ++++++++ tty/Terminal_windows.go | 2 ++ 4 files changed, 14 insertions(+) rename tty/{Terminal_darwin.go => Terminal_bsd.go} (88%) create mode 100644 tty/Terminal_other.go diff --git a/tty/Terminal_darwin.go b/tty/Terminal_bsd.go similarity index 88% rename from tty/Terminal_darwin.go rename to tty/Terminal_bsd.go index 0726279..15e06e0 100644 --- a/tty/Terminal_darwin.go +++ b/tty/Terminal_bsd.go @@ -1,3 +1,5 @@ +//go:build darwin || freebsd + package tty import "golang.org/x/sys/unix" diff --git a/tty/Terminal_linux.go b/tty/Terminal_linux.go index 4e44a64..023c564 100644 --- a/tty/Terminal_linux.go +++ b/tty/Terminal_linux.go @@ -1,3 +1,5 @@ +//go:build linux + package tty import "golang.org/x/sys/unix" diff --git a/tty/Terminal_other.go b/tty/Terminal_other.go new file mode 100644 index 0000000..3ea5ee8 --- /dev/null +++ b/tty/Terminal_other.go @@ -0,0 +1,8 @@ +//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_windows.go b/tty/Terminal_windows.go index c7355d1..0bf1927 100644 --- a/tty/Terminal_windows.go +++ b/tty/Terminal_windows.go @@ -1,3 +1,5 @@ +//go:build windows + package tty import "golang.org/x/sys/windows" From b8b0fbb57e26e853c423ba688a0ae5f044c794e8 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 25 Feb 2025 16:37:15 +0100 Subject: [PATCH 09/10] Updated module path --- Benchmarks_test.go | 2 +- Color_test.go | 2 +- LCH_test.go | 2 +- README.md | 4 ++-- RGB_test.go | 4 ++-- Terminal.go | 2 +- ansi/Code.go | 2 +- ansi/Code_test.go | 4 ++-- go.mod | 6 +++--- go.sum | 4 ++-- tty/Terminal_test.go | 2 +- 11 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Benchmarks_test.go b/Benchmarks_test.go index 012c8e0..64ee417 100644 --- a/Benchmarks_test.go +++ b/Benchmarks_test.go @@ -3,7 +3,7 @@ package color_test import ( "testing" - "git.akyoto.dev/go/color" + "git.urbach.dev/go/color" ) func BenchmarkRGB(b *testing.B) { diff --git a/Color_test.go b/Color_test.go index 8260a09..da84c14 100644 --- a/Color_test.go +++ b/Color_test.go @@ -3,7 +3,7 @@ package color_test import ( "testing" - "git.akyoto.dev/go/color" + "git.urbach.dev/go/color" ) func TestPrint(t *testing.T) { diff --git a/LCH_test.go b/LCH_test.go index f2d59a9..3cd09e0 100644 --- a/LCH_test.go +++ b/LCH_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "git.akyoto.dev/go/color" + "git.urbach.dev/go/color" ) func TestLCH(t *testing.T) { diff --git a/README.md b/README.md index fdca232..40cd44e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Adds color to your terminal output. ## Installation ```shell -go get git.akyoto.dev/go/color +go get git.urbach.dev/go/color ``` ## Usage @@ -52,7 +52,7 @@ BenchmarkPrintRaw-12 3105022 387.3 ns/op 0 ## License -Please see the [license documentation](https://akyoto.dev/license). +Please see the [license documentation](https://urbach.dev/license). ## Copyright diff --git a/RGB_test.go b/RGB_test.go index 86686e4..56ed903 100644 --- a/RGB_test.go +++ b/RGB_test.go @@ -3,8 +3,8 @@ package color_test import ( "testing" - "git.akyoto.dev/go/assert" - "git.akyoto.dev/go/color" + "git.urbach.dev/go/assert" + "git.urbach.dev/go/color" ) func TestRGB(t *testing.T) { diff --git a/Terminal.go b/Terminal.go index d36d963..e47cf17 100644 --- a/Terminal.go +++ b/Terminal.go @@ -3,7 +3,7 @@ package color import ( "os" - "git.akyoto.dev/go/color/tty" + "git.urbach.dev/go/color/tty" ) // Terminal indicates if we're in a terminal with truecolor support. diff --git a/ansi/Code.go b/ansi/Code.go index a0f8510..46c68bd 100644 --- a/ansi/Code.go +++ b/ansi/Code.go @@ -3,7 +3,7 @@ package ansi import ( "fmt" - "git.akyoto.dev/go/color" + "git.urbach.dev/go/color" ) // Code represents an ANSI escape code. diff --git a/ansi/Code_test.go b/ansi/Code_test.go index a9fb83a..64a1f47 100644 --- a/ansi/Code_test.go +++ b/ansi/Code_test.go @@ -3,8 +3,8 @@ package ansi_test import ( "testing" - "git.akyoto.dev/go/color" - "git.akyoto.dev/go/color/ansi" + "git.urbach.dev/go/color" + "git.urbach.dev/go/color/ansi" ) func TestPrintRaw(t *testing.T) { diff --git a/go.mod b/go.mod index e0d90d7..f5b705c 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ -module git.akyoto.dev/go/color +module git.urbach.dev/go/color -go 1.22.0 +go 1.24 require ( - git.akyoto.dev/go/assert v0.1.3 + git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf golang.org/x/sys v0.30.0 ) diff --git a/go.sum b/go.sum index ed6101f..c5592cb 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ -git.akyoto.dev/go/assert v0.1.3 h1:QwCUbmG4aZYsNk/OuRBz1zWVKmGlDUHhOnnDBfn8Qw8= -git.akyoto.dev/go/assert v0.1.3/go.mod h1:0GzMaM0eURuDwtGkJJkCsI7r2aUKr+5GmWNTFPgDocM= +git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf h1:BQWa5GKNUsA5CSUa/+UlFWYCEVe3IDDKRbVqBLK0mAE= +git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf/go.mod h1:y9jGII9JFiF1HNIju0u87OyPCt82xKCtqnAFyEreCDo= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/tty/Terminal_test.go b/tty/Terminal_test.go index be7406f..1f1a097 100644 --- a/tty/Terminal_test.go +++ b/tty/Terminal_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - "git.akyoto.dev/go/color/tty" + "git.urbach.dev/go/color/tty" ) func TestIsTerminal(t *testing.T) { From afc4092824a56cbf69a23af2026519b8ae2dd316 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 11 Mar 2025 14:24:41 +0100 Subject: [PATCH 10/10] Added HSL and HSV colors --- Benchmarks_test.go | 8 ++++---- Color.go | 14 +++++++------- Color_test.go | 6 ++++++ HSL.go | 29 +++++++++++++++++++++++++++++ HSL_test.go | 26 ++++++++++++++++++++++++++ HSV.go | 29 +++++++++++++++++++++++++++++ HSV_test.go | 26 ++++++++++++++++++++++++++ LCH.go | 7 +------ LCH_test.go | 10 ++++++---- README.md | 14 +++++++++----- RGB.go | 8 ++++++-- RGB_test.go | 13 +------------ go.mod | 5 +---- go.sum | 6 ++---- 14 files changed, 153 insertions(+), 48 deletions(-) create mode 100644 HSL.go create mode 100644 HSL_test.go create mode 100644 HSV.go create mode 100644 HSV_test.go diff --git a/Benchmarks_test.go b/Benchmarks_test.go index 64ee417..c811429 100644 --- a/Benchmarks_test.go +++ b/Benchmarks_test.go @@ -7,13 +7,13 @@ import ( ) func BenchmarkRGB(b *testing.B) { - for i := 0; i < b.N; i++ { + for b.Loop() { color.RGB(1.0, 1.0, 1.0) } } func BenchmarkLCH(b *testing.B) { - for i := 0; i < b.N; i++ { + for b.Loop() { color.LCH(0.5, 0.5, 0.0) } } @@ -22,7 +22,7 @@ func BenchmarkPrint(b *testing.B) { color.Terminal = true c := color.RGB(1.0, 1.0, 1.0) - for i := 0; i < b.N; i++ { + for b.Loop() { c.Print("") } } @@ -31,7 +31,7 @@ func BenchmarkPrintRaw(b *testing.B) { color.Terminal = false c := color.RGB(1.0, 1.0, 1.0) - for i := 0; i < b.N; i++ { + for b.Loop() { c.Print("") } } diff --git a/Color.go b/Color.go index cb83a0a..5b007c9 100644 --- a/Color.go +++ b/Color.go @@ -4,11 +4,11 @@ import ( "fmt" ) -// Color represents an RGB color. +// Color represents an sRGB color. type Color struct { - R Value - G Value - B Value + R byte + G byte + B byte } // Print writes the text in the given color to standard output. @@ -18,7 +18,7 @@ func (c Color) Print(args ...any) { return } - fmt.Printf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", byte(c.R*255), byte(c.G*255), byte(c.B*255), fmt.Sprint(args...)) + 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. @@ -28,7 +28,7 @@ func (c Color) Printf(format string, args ...any) { return } - fmt.Printf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", byte(c.R*255), byte(c.G*255), byte(c.B*255), fmt.Sprintf(format, args...)) + fmt.Printf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", c.R, c.G, c.B, fmt.Sprintf(format, args...)) } // Println writes the text in the given color to standard output and appends a newline. @@ -38,5 +38,5 @@ func (c Color) Println(args ...any) { return } - fmt.Printf("\x1b[38;2;%d;%d;%dm%s\n\x1b[0m", byte(c.R*255), byte(c.G*255), byte(c.B*255), fmt.Sprint(args...)) + fmt.Printf("\x1b[38;2;%d;%d;%dm%s\n\x1b[0m", c.R, c.G, c.B, fmt.Sprint(args...)) } diff --git a/Color_test.go b/Color_test.go index da84c14..1abfa15 100644 --- a/Color_test.go +++ b/Color_test.go @@ -8,12 +8,14 @@ import ( 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") @@ -22,12 +24,14 @@ func TestPrint(t *testing.T) { 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") @@ -36,12 +40,14 @@ func TestPrintf(t *testing.T) { 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") diff --git a/HSL.go b/HSL.go new file mode 100644 index 0000000..f5cda9a --- /dev/null +++ b/HSL.go @@ -0,0 +1,29 @@ +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 new file mode 100644 index 0000000..0c3e654 --- /dev/null +++ b/HSL_test.go @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..2efa5fd --- /dev/null +++ b/HSV.go @@ -0,0 +1,29 @@ +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 new file mode 100644 index 0000000..c2b4bd3 --- /dev/null +++ b/HSV_test.go @@ -0,0 +1,26 @@ +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 fb2b662..c27d4df 100644 --- a/LCH.go +++ b/LCH.go @@ -19,12 +19,7 @@ func LCH(lightness Value, chroma Value, hue Value) Color { b *= chroma r, g, b := oklabToLinearRGB(lightness, a, b) - - r = sRGB(r) - g = sRGB(g) - b = sRGB(b) - - return Color{r, g, b} + return RGB(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 3cd09e0..59e9097 100644 --- a/LCH_test.go +++ b/LCH_test.go @@ -25,20 +25,22 @@ func TestLCH(t *testing.T) { } 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 { - c := color.LCH(color.Value(lightness)*0.05, color.Value(chroma)*0.05, color.Value(hue)*4.4) - testColorRange(t, c) - c.Print("█") + 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("█") } fmt.Println() diff --git a/README.md b/README.md index 40cd44e..56fe49a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ Adds color to your terminal output. ## Features - ANSI colors +- HSL colors +- HSV colors - LCH colors - RGB colors @@ -35,6 +37,8 @@ blue.Println("blue text") PASS: TestPrint PASS: TestPrintf PASS: TestPrintln +PASS: TestHSLSpectrum +PASS: TestHSVSpectrum PASS: TestLCH PASS: TestLCHSpectrum PASS: TestRGB @@ -44,10 +48,10 @@ coverage: 100.0% of statements ## Benchmarks ``` -BenchmarkRGB-12 1000000000 0.3141 ns/op 0 B/op 0 allocs/op -BenchmarkLCH-12 4780176 250.9 ns/op 0 B/op 0 allocs/op -BenchmarkPrint-12 375394 3343 ns/op 0 B/op 0 allocs/op -BenchmarkPrintRaw-12 3105022 387.3 ns/op 0 B/op 0 allocs/op +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 ``` ## License @@ -56,4 +60,4 @@ Please see the [license documentation](https://urbach.dev/license). ## Copyright -© 2024 Eduard Urbach +© 2024 Eduard Urbach \ No newline at end of file diff --git a/RGB.go b/RGB.go index f882f07..3d94abc 100644 --- a/RGB.go +++ b/RGB.go @@ -2,9 +2,13 @@ package color import "math" -// RGB creates a new color with red, green and blue values in the range of 0.0 to 1.0. +// RGB creates a new sRGB color. func RGB(r Value, g Value, b Value) Color { - return Color{r, g, b} + 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. diff --git a/RGB_test.go b/RGB_test.go index 56ed903..a1f4481 100644 --- a/RGB_test.go +++ b/RGB_test.go @@ -3,12 +3,12 @@ package color_test import ( "testing" - "git.urbach.dev/go/assert" "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), @@ -24,17 +24,6 @@ func TestRGB(t *testing.T) { } 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/go.mod b/go.mod index f5b705c..c3152ec 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,4 @@ module git.urbach.dev/go/color go 1.24 -require ( - git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf - golang.org/x/sys v0.30.0 -) +require golang.org/x/sys v0.31.0 diff --git a/go.sum b/go.sum index c5592cb..c55261f 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,2 @@ -git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf h1:BQWa5GKNUsA5CSUa/+UlFWYCEVe3IDDKRbVqBLK0mAE= -git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf/go.mod h1:y9jGII9JFiF1HNIju0u87OyPCt82xKCtqnAFyEreCDo= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=