Compare commits

..

No commits in common. "afc4092824a56cbf69a23af2026519b8ae2dd316" and "cd70550a0a4e6052452906d61afb8454601de8e0" have entirely different histories.

25 changed files with 131 additions and 424 deletions

View file

@ -1,37 +1,38 @@
package color_test package color_test
import ( import (
"io"
"testing" "testing"
"git.urbach.dev/go/color" "git.akyoto.dev/go/color"
) )
func BenchmarkRGB(b *testing.B) { func BenchmarkRGB(b *testing.B) {
for b.Loop() { for i := 0; i < b.N; i++ {
color.RGB(1.0, 1.0, 1.0) color.RGB(1.0, 1.0, 1.0)
} }
} }
func BenchmarkLCH(b *testing.B) { func BenchmarkLCH(b *testing.B) {
for b.Loop() { for i := 0; i < b.N; i++ {
color.LCH(0.5, 0.5, 0.0) color.LCH(0.5, 0.5, 0.0)
} }
} }
func BenchmarkPrint(b *testing.B) { func BenchmarkFprintColorized(b *testing.B) {
color.Terminal = true color.Terminal = true
c := color.RGB(1.0, 1.0, 1.0) c := color.RGB(1.0, 1.0, 1.0)
for b.Loop() { for i := 0; i < b.N; i++ {
c.Print("") c.Fprint(io.Discard, "")
} }
} }
func BenchmarkPrintRaw(b *testing.B) { func BenchmarkFprintRaw(b *testing.B) {
color.Terminal = false color.Terminal = false
c := color.RGB(1.0, 1.0, 1.0) c := color.RGB(1.0, 1.0, 1.0)
for b.Loop() { for i := 0; i < b.N; i++ {
c.Print("") c.Fprint(io.Discard, "")
} }
} }

View file

@ -2,41 +2,50 @@ package color
import ( import (
"fmt" "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 { type Color struct {
R byte R Value
G byte G Value
B byte 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. // Print writes the text in the given color to standard output.
func (c Color) Print(args ...any) { func (c Color) Print(text string) {
if !Terminal || !TrueColor { if !Terminal {
fmt.Print(args...) fmt.Print(text)
return return
} }
fmt.Printf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", c.R, c.G, c.B, fmt.Sprint(args...)) fmt.Printf(format, byte(c.R*255), byte(c.G*255), byte(c.B*255), text)
}
// 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...))
} }
// Println writes the text in the given color to standard output and appends a newline. // Println writes the text in the given color to standard output and appends a newline.
func (c Color) Println(args ...any) { func (c Color) Println(text string) {
if !Terminal || !TrueColor { if !Terminal {
fmt.Println(args...) fmt.Println(text)
return 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)
} }

View file

@ -1,55 +1,83 @@
package color_test package color_test
import ( import (
"io"
"testing" "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) { func TestPrint(t *testing.T) {
color.Terminal = true color.Terminal = true
color.TrueColor = true
color.RGB(1, 0, 0).Print("red\n") color.RGB(1, 0, 0).Print("red\n")
color.RGB(0, 1, 0).Print("green\n") color.RGB(0, 1, 0).Print("green\n")
color.RGB(0, 0, 1).Print("blue\n") color.RGB(0, 0, 1).Print("blue\n")
color.Terminal = false color.Terminal = false
color.TrueColor = false
color.RGB(1, 0, 0).Print("red\n") color.RGB(1, 0, 0).Print("red\n")
color.RGB(0, 1, 0).Print("green\n") color.RGB(0, 1, 0).Print("green\n")
color.RGB(0, 0, 1).Print("blue\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) { func TestPrintln(t *testing.T) {
color.Terminal = true color.Terminal = true
color.TrueColor = true
color.RGB(1, 0, 0).Println("red") color.RGB(1, 0, 0).Println("red")
color.RGB(0, 1, 0).Println("green") color.RGB(0, 1, 0).Println("green")
color.RGB(0, 0, 1).Println("blue") color.RGB(0, 0, 1).Println("blue")
color.Terminal = false color.Terminal = false
color.TrueColor = false
color.RGB(1, 0, 0).Println("red") color.RGB(1, 0, 0).Println("red")
color.RGB(0, 1, 0).Println("green") color.RGB(0, 1, 0).Println("green")
color.RGB(0, 0, 1).Println("blue") 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)
}

29
HSL.go
View file

@ -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)
}

View file

@ -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()
}
}

29
HSV.go
View file

@ -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)
}

View file

@ -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()
}
}

7
LCH.go
View file

@ -19,7 +19,12 @@ func LCH(lightness Value, chroma Value, hue Value) Color {
b *= chroma b *= chroma
r, g, b := oklabToLinearRGB(lightness, a, b) 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. // findChromaInSRGB tries to find the closest chroma that can be represented in sRGB color space.

View file

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"testing" "testing"
"git.urbach.dev/go/color" "git.akyoto.dev/go/color"
) )
func TestLCH(t *testing.T) { func TestLCH(t *testing.T) {
@ -18,29 +18,27 @@ func TestLCH(t *testing.T) {
"red": color.LCH(0.75, 1.0, 40), "red": color.LCH(0.75, 1.0, 40),
"orange": color.LCH(0.75, 1.0, 60), "orange": color.LCH(0.75, 1.0, 60),
"yellow": color.LCH(0.9, 1.0, 100), "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), "blue": color.LCH(0.75, 1.0, 260),
"cyan": color.LCH(0.75, 1.0, 210), "cyan": color.LCH(0.75, 1.0, 210),
"magenta": color.LCH(0.75, 1.0, 320), "magenta": color.LCH(0.75, 1.0, 320),
} }
for name, c := range lchColors { for name, c := range lchColors {
testColorRange(t, c)
c.Println("█ " + name) c.Println("█ " + name)
} }
} }
func TestLCHSpectrum(t *testing.T) { func TestLCHSpectrum(t *testing.T) {
color.Terminal = true color.Terminal = true
color.TrueColor = true
for chroma := range 4 { for chroma := range 4 {
for lightness := range 21 { for lightness := range 21 {
for hue := range 80 { for hue := range 80 {
l := color.Value(lightness) * 0.05 c := color.LCH(color.Value(lightness)*0.05, color.Value(chroma)*0.05, color.Value(hue)*4.4)
c := color.Value(chroma) * 0.05 testColorRange(t, c)
h := color.Value(hue) * 4.4 c.Print("█")
col := color.LCH(l, c, h)
col.Print("█")
} }
fmt.Println() fmt.Println()

View file

@ -4,59 +4,51 @@ Adds color to your terminal output.
## Features ## Features
- ANSI colors - RGB color space
- HSL colors - LCH color space (oklch)
- HSV colors - Truecolor terminal output
- LCH colors - Zero dependencies (excluding tests)
- RGB colors
## Installation ## Installation
```shell ```shell
go get git.urbach.dev/go/color go get git.akyoto.dev/go/color
``` ```
## Usage ## Usage
```go ```go
// ANSI red := color.RGB(1.0, 0.0, 0.0)
ansi.Red.Println("red text") red.Println("red text")
// LCH orange := color.LCH(0.7, 1.0, 65)
green := color.LCH(0.5, 1.0, 135) orange.Println("orange text")
green.Println("green text")
// RGB
blue := color.RGB(0, 0, 1)
blue.Println("blue text")
``` ```
## Tests ## Tests
``` ```
PASS: TestFprint
PASS: TestPrint PASS: TestPrint
PASS: TestPrintf
PASS: TestPrintln PASS: TestPrintln
PASS: TestHSLSpectrum PASS: TestRGB
PASS: TestHSVSpectrum
PASS: TestLCH PASS: TestLCH
PASS: TestLCHSpectrum PASS: TestLCHSpectrum
PASS: TestRGB
coverage: 100.0% of statements coverage: 100.0% of statements
``` ```
## Benchmarks ## Benchmarks
``` ```
BenchmarkRGB-20 100000000 14.88 ns/op 0 B/op 0 allocs/op BenchmarkRGB-12 1000000000 0.3132 ns/op 0 B/op 0 allocs/op
BenchmarkLCH-20 5075756 227.8 ns/op 0 B/op 0 allocs/op BenchmarkLCH-12 4802006 249.8 ns/op 0 B/op 0 allocs/op
BenchmarkPrint-20 1587134 755.5 ns/op 0 B/op 0 allocs/op BenchmarkFprintColorized-12 6356535 188.4 ns/op 0 B/op 0 allocs/op
BenchmarkPrintRaw-20 3166090 361.5 ns/op 0 B/op 0 allocs/op BenchmarkFprintRaw-12 27374659 43.76 ns/op 0 B/op 0 allocs/op
``` ```
## License ## License
Please see the [license documentation](https://urbach.dev/license). Please see the [license documentation](https://akyoto.dev/license).
## Copyright ## Copyright

View file

@ -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)
}
}

View file

@ -2,12 +2,13 @@ package color
import ( import (
"os" "os"
"git.urbach.dev/go/color/tty"
) )
// Terminal indicates if we're in a terminal with truecolor support. // These constants represent the escape codes needed to display color in terminals.
var Terminal = tty.IsTerminal(os.Stdout.Fd()) 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. // Terminal is a boolean that indicates if we're in a terminal with truecolor support.
var TrueColor = os.Getenv("COLORTERM") == "truecolor" var Terminal = os.Getenv("COLORTERM") == "truecolor"

View file

@ -1,4 +0,0 @@
package color
// Value is a type definition for the data type of a single color component.
type Value = float64

View file

@ -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...))
}

View file

@ -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")
}

View file

@ -1,12 +0,0 @@
package ansi
const (
Black Code = iota + 30
Red
Green
Yellow
Blue
Magenta
Cyan
White
)

View file

@ -1,14 +0,0 @@
package ansi
const (
Reset Code = iota
Bold
Dim
Italic
Underline
Blink
BlinkFast
Reverse
Hidden
Strikethrough
)

6
go.mod
View file

@ -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

4
go.sum
View file

@ -1,2 +1,2 @@
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= git.akyoto.dev/go/assert v0.1.3 h1:QwCUbmG4aZYsNk/OuRBz1zWVKmGlDUHhOnnDBfn8Qw8=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= git.akyoto.dev/go/assert v0.1.3/go.mod h1:0GzMaM0eURuDwtGkJJkCsI7r2aUKr+5GmWNTFPgDocM=

View file

@ -2,15 +2,6 @@ package color
import "math" 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. // 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 { func inSRGB(l Value, a Value, b Value, chroma Value) bool {
r, g, b := oklabToLinearRGB(l, a*chroma, b*chroma) r, g, b := oklabToLinearRGB(l, a*chroma, b*chroma)

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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())
}

View file

@ -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
}