aboutsummaryrefslogtreecommitdiff
path: root/internal/color
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-27 16:50:41 -0400
committerhistoria <[not public]>2026-06-27 16:50:41 -0400
commit988b006a5bb9edb6465653566e7bdf8175347b8c (patch)
tree3a1fceff2ee80c11b5dafcc49c13ababe021e880 /internal/color
parent1cab9ca20e24742f770a676f84e4ed9f1636f297 (diff)
downloadthehouseoficarus-988b006a5bb9edb6465653566e7bdf8175347b8c.tar.gz
feat: one-way map links, blocked paths on map, map grid startup validation, user colors for maps
Diffstat (limited to 'internal/color')
-rw-r--r--internal/color/color.go37
-rw-r--r--internal/color/color_test.go64
2 files changed, 98 insertions, 3 deletions
diff --git a/internal/color/color.go b/internal/color/color.go
index 22fa143..13fca69 100644
--- a/internal/color/color.go
+++ b/internal/color/color.go
@@ -243,6 +243,24 @@ func NoColor() ColorSpec {
return ColorSpec{Fg: -1, Bg: -1}
}
+// Average returns a ColorSpec whose foreground is the RGB midpoint of the two
+// inputs' foregrounds. If only one input has a foreground, that one is used; if
+// neither does, NoColor is returned. Styles and background are not carried over.
+func Average(a, b ColorSpec) ColorSpec {
+ if a.Fg < 0 && b.Fg < 0 {
+ return NoColor()
+ }
+ if a.Fg < 0 {
+ return ColorSpec{Fg: b.Fg, Bg: -1}
+ }
+ if b.Fg < 0 {
+ return ColorSpec{Fg: a.Fg, Bg: -1}
+ }
+ r1, g1, b1 := Xterm256ToRGB(a.Fg)
+ r2, g2, b2 := Xterm256ToRGB(b.Fg)
+ return ColorSpec{Fg: NearestXterm256((r1+r2)/2, (g1+g2)/2, (b1+b2)/2), Bg: -1}
+}
+
func Parse(input string) ColorSpec {
spec := ColorSpec{Fg: -1, Bg: -1}
for _, token := range strings.Fields(input) {
@@ -254,14 +272,14 @@ func Parse(input string) ColorSpec {
case token == "underline":
spec.Underline = true
case strings.HasPrefix(token, "bg:"):
- if n, err := strconv.Atoi(strings.TrimPrefix(token, "bg:")); err == nil && n >= 0 && n <= 255 {
+ if n, ok := parseColorIndex(strings.TrimPrefix(token, "bg:")); ok {
spec.Bg = n
}
case strings.HasPrefix(token, "g:"):
parts := strings.Split(token[2:], ",")
var stops []int
for _, p := range parts {
- if n, err := strconv.Atoi(strings.TrimSpace(p)); err == nil && n >= 0 && n <= 255 {
+ if n, ok := parseColorIndex(strings.TrimSpace(p)); ok {
stops = append(stops, n)
}
}
@@ -269,7 +287,7 @@ func Parse(input string) ColorSpec {
spec.Gradient = stops
}
default:
- if n, err := strconv.Atoi(token); err == nil && n >= 0 && n <= 255 {
+ if n, ok := parseColorIndex(token); ok {
spec.Fg = n
}
}
@@ -277,6 +295,19 @@ func Parse(input string) ColorSpec {
return spec
}
+// parseColorIndex parses a 1-2 digit hexadecimal color index (00-FF, case
+// insensitive) into its 0-255 value. Out-of-range or non-hex input fails.
+func parseColorIndex(s string) (int, bool) {
+ if s == "" {
+ return 0, false
+ }
+ n, err := strconv.ParseUint(s, 16, 0)
+ if err != nil || n > 255 {
+ return 0, false
+ }
+ return int(n), true
+}
+
func (s ColorSpec) Empty() bool {
return s.Fg < 0 && s.Bg < 0 && !s.Bold && !s.Dim && !s.Underline && len(s.Gradient) == 0
}
diff --git a/internal/color/color_test.go b/internal/color/color_test.go
index c1f5e5b..bffc0d7 100644
--- a/internal/color/color_test.go
+++ b/internal/color/color_test.go
@@ -53,3 +53,67 @@ func TestWrapANSISplitsOnNewlines(t *testing.T) {
t.Errorf("WrapANSI newline handling = %q, want %q", got, want)
}
}
+
+func TestParseHexIndex(t *testing.T) {
+ cases := []struct {
+ in string
+ want int
+ }{
+ {"00", 0},
+ {"0F", 15},
+ {"0f", 15},
+ {"D0", 208},
+ {"FF", 255},
+ {"F", 15},
+ }
+ for _, c := range cases {
+ if got := Parse(c.in).Fg; got != c.want {
+ t.Errorf("Parse(%q).Fg = %d, want %d", c.in, got, c.want)
+ }
+ }
+}
+
+func TestParseHexRejectsInvalid(t *testing.T) {
+ for _, in := range []string{"ZZ", "100", "G1"} {
+ if got := Parse(in).Fg; got != -1 {
+ t.Errorf("Parse(%q).Fg = %d, want -1 (rejected)", in, got)
+ }
+ }
+}
+
+func TestParseHexBgAndGradient(t *testing.T) {
+ s := Parse("D0 bg:00 g:C4,52 bold")
+ if s.Fg != 208 {
+ t.Errorf("Fg = %d, want 208", s.Fg)
+ }
+ if s.Bg != 0 {
+ t.Errorf("Bg = %d, want 0", s.Bg)
+ }
+ if !s.Bold {
+ t.Error("Bold not set")
+ }
+ if len(s.Gradient) != 2 || s.Gradient[0] != 196 || s.Gradient[1] != 82 {
+ t.Errorf("Gradient = %v, want [196 82]", s.Gradient)
+ }
+}
+
+func TestAverage(t *testing.T) {
+ // black (0) and white (15) average to a mid grey.
+ mid := Average(Parse("00"), Parse("0F"))
+ if mid.Fg < 0 {
+ t.Fatalf("expected a foreground, got %d", mid.Fg)
+ }
+ r, g, b := Xterm256ToRGB(mid.Fg)
+ if r > 200 || r < 55 || g > 200 || g < 55 || b > 200 || b < 55 {
+ t.Errorf("midpoint of black/white not grey: rgb=%d,%d,%d (idx %d)", r, g, b, mid.Fg)
+ }
+
+ // one side colorless -> use the other.
+ if got := Average(NoColor(), Parse("D0")); got.Fg != 208 {
+ t.Errorf("Average(none, D0).Fg = %d, want 208", got.Fg)
+ }
+ // both colorless -> NoColor.
+ if got := Average(NoColor(), NoColor()); got.Fg != -1 {
+ t.Errorf("Average(none, none).Fg = %d, want -1", got.Fg)
+ }
+}