commit cdda64c

delthas  ·  2025-02-19 16:03:59 +0000 UTC
parent 1e2ffe2
Use alternative gray color on color schemes with bg == gray

With these heuristics, "gray" text should always be readable
regardless of the theme, even for 8-color themes.

Fixes: https://todo.sr.ht/~delthas/senpai/160
11 files changed,  +91, -63
M app.go
M go.mod
M go.sum
M app.go
+5, -4
 1@@ -179,7 +179,7 @@ func NewApp(cfg Config) (app *App, err error) {
 2 
 3 	mouse := cfg.Mouse
 4 
 5-	app.win, err = ui.New(ui.Config{
 6+	app.win, app.cfg.Colors, err = ui.New(ui.Config{
 7 		NickColWidth:     cfg.NickColWidth,
 8 		ChanColWidth:     cfg.ChanColWidth,
 9 		ChanColEnabled:   cfg.ChanColEnabled,
10@@ -199,6 +199,7 @@ func NewApp(cfg Config) (app *App, err error) {
11 	if err != nil {
12 		return
13 	}
14+
15 	ui.NotifyStart(func(ev *ui.NotifyEvent) {
16 		app.events <- event{
17 			src:     "*",
18@@ -1865,7 +1866,7 @@ func (app *App) formatEvent(ev irc.Event) ui.Line {
19 		var body ui.StyledStringBuilder
20 		body.Grow(len(ev.User) + 1)
21 		body.SetStyle(vaxis.Style{
22-			Foreground: vaxis.IndexColor(2),
23+			Foreground: ui.ColorGreen,
24 		})
25 		body.WriteByte('+')
26 		body.SetStyle(vaxis.Style{
27@@ -1959,7 +1960,7 @@ func (app *App) formatEvent(ev irc.Event) ui.Line {
28 					body.WriteByte('-')
29 				} else {
30 					body.SetStyle(vaxis.Style{
31-						Foreground: vaxis.IndexColor(2),
32+						Foreground: ui.ColorGreen,
33 					})
34 					body.WriteByte('+')
35 				}
36@@ -2060,7 +2061,7 @@ func (app *App) formatMessage(s *irc.Session, ev irc.MessageEvent) (buffer strin
37 	}
38 
39 	head := ev.User
40-	headColor := vaxis.IndexColor(15)
41+	var headColor vaxis.Color
42 	if isAction || isNotice {
43 		head = "*"
44 	} else {
+1, -1
1@@ -560,7 +560,7 @@ func commandDoNames(app *App, args []string) (err error) {
2 	for _, name := range s.Names(buffer) {
3 		if name.PowerLevel != "" {
4 			sb.SetStyle(vaxis.Style{
5-				Foreground: vaxis.IndexColor(2),
6+				Foreground: ui.ColorGreen,
7 			})
8 			sb.WriteString(name.PowerLevel)
9 			sb.SetStyle(vaxis.Style{
+6, -6
 1@@ -73,7 +73,7 @@ func parseColor(s string, c *vaxis.Color) error {
 2 	}
 3 
 4 	if code == -1 {
 5-		*c = vaxis.Color(0)
 6+		*c = ui.ColorDefault
 7 		return nil
 8 	}
 9 
10@@ -149,13 +149,13 @@ func Defaults() Config {
11 		TextMaxWidth:     0,
12 		StatusEnabled:    true,
13 		Colors: ui.ConfigColors{
14-			Status: ui.ColorGray,
15-			Prompt: vaxis.Color(0),
16-			Unread: vaxis.Color(0),
17+			Status: ui.ColorDefault, // Overriden by UI later.
18+			Prompt: ui.ColorDefault,
19+			Unread: ui.ColorDefault,
20 			Nicks: ui.ColorScheme{
21 				Type:   ui.ColorSchemeBase,
22-				Others: vaxis.Color(0),
23-				Self:   vaxis.Color(9),
24+				Others: ui.ColorDefault,
25+				Self:   ui.ColorRed,
26 			},
27 		},
28 		Debug:             false,
M go.mod
+1, -1
1@@ -4,7 +4,7 @@ go 1.18
2 
3 require (
4 	codeberg.org/emersion/go-scfg v0.1.0
5-	git.sr.ht/~rockorager/vaxis v0.12.0
6+	git.sr.ht/~rockorager/vaxis v0.12.1-0.20250218200016-0e88f197413a
7 	github.com/delthas/go-libnp v0.0.0-20250105150050-96674b98150e
8 	github.com/delthas/go-localeinfo v0.0.0-20240813094314-e5413e186769
9 	github.com/disintegration/imaging v1.6.2
M go.sum
+2, -2
 1@@ -1,7 +1,7 @@
 2 codeberg.org/emersion/go-scfg v0.1.0 h1:6dnGU0ZI4gX+O5rMjwhoaySItzHG710eXL5TIQKl+uM=
 3 codeberg.org/emersion/go-scfg v0.1.0/go.mod h1:0nooW1ufBB4SlJEdTtiVN9Or+bnNM1icOkQ6Tbrq6O0=
 4-git.sr.ht/~rockorager/vaxis v0.12.0 h1:7zM2If+mHTliDMLWhermHLnIfNkEIuDs3sHYh1MjdF8=
 5-git.sr.ht/~rockorager/vaxis v0.12.0/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
 6+git.sr.ht/~rockorager/vaxis v0.12.1-0.20250218200016-0e88f197413a h1:duJRKIJCDSWNUx1A7GmlGK2HcRxbVFrFiXSqAW2EBHE=
 7+git.sr.ht/~rockorager/vaxis v0.12.1-0.20250218200016-0e88f197413a/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
 8 github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
 9 github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
10 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+8, -8
 1@@ -828,7 +828,7 @@ func (bs *BufferList) DrawVerticalBufferList(vx *Vaxis, x0, y0, width, height in
 2 	}
 3 
 4 	width--
 5-	drawVerticalLine(vx, x0+width, y0, height)
 6+	bs.ui.drawVerticalLine(vx, x0+width, y0, height)
 7 	clearArea(vx, x0, y0, width, height)
 8 
 9 	indexPadding := 1 + int(math.Ceil(math.Log10(float64(len(bs.list)))))
10@@ -844,7 +844,7 @@ func (bs *BufferList) DrawVerticalBufferList(vx *Vaxis, x0, y0, width, height in
11 		if bi == bs.current || bi == bs.clicked {
12 			st.Attribute |= vaxis.AttrReverse
13 		} else if b.muted {
14-			st.Foreground = ColorGray
15+			st.Foreground = bs.ui.config.Colors.Gray
16 		}
17 
18 		var title string
19@@ -859,7 +859,7 @@ func (bs *BufferList) DrawVerticalBufferList(vx *Vaxis, x0, y0, width, height in
20 				continue
21 			}
22 			indexSt := st
23-			indexSt.Foreground = ColorGray
24+			indexSt.Foreground = bs.ui.config.Colors.Gray
25 			indexText := fmt.Sprintf("%d:", bi+1)
26 			printString(vx, &x, y, Styled(indexText, indexSt))
27 			x = x0 + indexPadding
28@@ -1043,7 +1043,7 @@ func (bs *BufferList) DrawHorizontalBufferList(vx *Vaxis, x0, y0, width int, off
29 		if i == bs.clicked {
30 			st.Attribute |= vaxis.AttrReverse
31 		} else if b.muted {
32-			st.Foreground = ColorGray
33+			st.Foreground = bs.ui.config.Colors.Gray
34 		}
35 
36 		var title string
37@@ -1171,7 +1171,7 @@ func (bs *BufferList) DrawTimeline(ui *UI, x0, y0, nickColWidth int) {
38 		}
39 	}
40 	y0++
41-	drawHorizontalLine(vx, x0, y0, bs.tlInnerWidth+nickColWidth+9)
42+	bs.ui.drawHorizontalLine(vx, x0, y0, bs.tlInnerWidth+nickColWidth+9)
43 	y0++
44 
45 	if bs.textWidth < bs.tlInnerWidth {
46@@ -1195,10 +1195,10 @@ func (bs *BufferList) DrawTimeline(ui *UI, x0, y0, nickColWidth int) {
47 			if isRead && yi > y0 {
48 				yi--
49 				st := vaxis.Style{
50-					Foreground: ColorGray,
51+					Foreground: bs.ui.config.Colors.Gray,
52 				}
53 				printIdent(vx, x0+7, yi, nickColWidth, Styled("--", st))
54-				drawHorizontalLine(vx, x0, yi, 9+nickColWidth+bs.tlInnerWidth)
55+				bs.ui.drawHorizontalLine(vx, x0, yi, 9+nickColWidth+bs.tlInnerWidth)
56 				rulerDrawn = true
57 			}
58 		}
59@@ -1228,7 +1228,7 @@ func (bs *BufferList) DrawTimeline(ui *UI, x0, y0, nickColWidth int) {
60 			}
61 			if showTime {
62 				st := vaxis.Style{
63-					Foreground: ColorGray,
64+					Foreground: bs.ui.config.Colors.Gray,
65 				}
66 				printTime(vx, x0, yi, st, line.At.Local())
67 			}
+2, -1
 1@@ -7,8 +7,9 @@ import (
 2 	"git.sr.ht/~rockorager/vaxis"
 3 )
 4 
 5+var ColorDefault = vaxis.Color(0)
 6+var ColorGreen = vaxis.IndexColor(2)
 7 var ColorRed = vaxis.IndexColor(9)
 8-var ColorGray = vaxis.IndexColor(8)
 9 
10 type ColorSchemeType int
11 
+0, -14
 1@@ -261,17 +261,3 @@ func printTime(vx *Vaxis, x int, y int, st vaxis.Style, t time.Time) {
 2 func clearArea(vx *Vaxis, x0, y0, width, height int) {
 3 	vx.window.New(x0, y0, width, height).Clear()
 4 }
 5-
 6-func drawHorizontalLine(vx *Vaxis, x0, y, width int) {
 7-	for x := x0; x < x0+width; x++ {
 8-		setCell(vx, x, y, '─', vaxis.Style{
 9-			Foreground: ColorGray,
10-		})
11-	}
12-}
13-
14-func drawVerticalLine(vx *Vaxis, x, y0, height int) {
15-	for y := y0; y < y0+height; y++ {
16-		setCell(vx, x, y, '│', vaxis.Style{})
17-	}
18-}
+1, -2
 1@@ -624,8 +624,7 @@ func (e *Editor) Draw(vx *Vaxis, x0, y int, hint string) {
 2 		i := 0
 3 		for i < len(display) {
 4 			s := vaxis.Style{
 5-				Background: vaxis.IndexColor(0),
 6-				Attribute:  vaxis.AttrReverse,
 7+				Attribute: vaxis.AttrReverse,
 8 			}
 9 			if ci+autoOff == e.autoCacheIdx {
10 				s.Attribute |= vaxis.AttrBold
+10, -12
 1@@ -47,7 +47,7 @@ var hexCodes = []uint32{
 2 
 3 func colorFromCode(code int) (color vaxis.Color) {
 4 	if code < 0 || 99 <= code {
 5-		color = vaxis.Color(0)
 6+		color = ColorDefault
 7 	} else if code < 16 {
 8 		color = baseCodes[code]
 9 	} else {
10@@ -219,16 +219,16 @@ func parseColor(raw string) (fg, bg vaxis.Color, n int) {
11 	raw = raw[n:]
12 
13 	if len(raw) == 0 || raw[0] != ',' {
14-		return fg, vaxis.Color(0), n
15+		return fg, ColorDefault, n
16 	}
17 
18 	n++
19 	bg, p := parseColorNumber(raw[1:])
20 	n += p
21 
22-	if bg == vaxis.Color(0) {
23+	if bg == ColorDefault {
24 		// Lone comma, do not parse as part of a color code.
25-		return fg, vaxis.Color(0), n - 1
26+		return fg, ColorDefault, n - 1
27 	}
28 
29 	return fg, bg, n
30@@ -253,16 +253,16 @@ func parseHexColor(raw string) (fg, bg vaxis.Color, n int) {
31 	raw = raw[n:]
32 
33 	if len(raw) == 0 || raw[0] != ',' {
34-		return fg, vaxis.Color(0), n
35+		return fg, ColorDefault, n
36 	}
37 
38 	n++
39 	bg, p := parseHexColorNumber(raw[1:])
40 	n += p
41 
42-	if bg == vaxis.Color(0) {
43+	if bg == ColorDefault {
44 		// Lone comma, do not parse as part of a color code.
45-		return fg, vaxis.Color(0), n - 1
46+		return fg, ColorDefault, n - 1
47 	}
48 
49 	return fg, bg, n
50@@ -302,11 +302,9 @@ func IRCString(raw string) StyledString {
51 			}
52 			raw = raw[n:]
53 			if n == 0 {
54-				// Both `fg` and `bg` are equal to
55-				// tcell.ColorDefault.
56-				current.Foreground = vaxis.Color(0)
57-				current.Background = vaxis.Color(0)
58-			} else if bg == vaxis.Color(0) {
59+				current.Foreground = ColorDefault
60+				current.Background = ColorDefault
61+			} else if bg == ColorDefault {
62 				current.Foreground = fg
63 			} else {
64 				current.Foreground = fg
+55, -12
  1@@ -3,6 +3,7 @@ package ui
  2 import (
  3 	"fmt"
  4 	"image"
  5+	"reflect"
  6 	"strings"
  7 	"sync/atomic"
  8 	"time"
  9@@ -29,6 +30,7 @@ type Config struct {
 10 }
 11 
 12 type ConfigColors struct {
 13+	Gray   vaxis.Color
 14 	Status vaxis.Color
 15 	Prompt vaxis.Color
 16 	Unread vaxis.Color
 17@@ -79,7 +81,7 @@ type UI struct {
 18 	colorThemeMode vaxis.ColorThemeMode
 19 }
 20 
 21-func New(config Config) (ui *UI, err error) {
 22+func New(config Config) (ui *UI, colors ConfigColors, err error) {
 23 	ui = &UI{
 24 		config:        config,
 25 		clickEvents:   make([]clickEvent, 0, 128),
 26@@ -105,7 +107,8 @@ func New(config Config) (ui *UI, err error) {
 27 		window: vx.Window(),
 28 	}
 29 
 30-	if bg := ui.vx.QueryBackground().Params(); len(bg) == 3 {
 31+	bg := ui.vx.QueryBackground().Params()
 32+	if len(bg) == 3 {
 33 		if (int(bg[0])+int(bg[1])+int(bg[2]))/3 > 127 {
 34 			ui.colorThemeMode = vaxis.LightMode
 35 		} else {
 36@@ -115,6 +118,32 @@ func New(config Config) (ui *UI, err error) {
 37 		ui.colorThemeMode = vaxis.DarkMode
 38 	}
 39 
 40+	ui.config.Colors.Gray = vaxis.IndexColor(8)
 41+	black := ui.vx.QueryColor(vaxis.IndexColor(uint8(0))).Params()
 42+	gray := ui.vx.QueryColor(vaxis.IndexColor(uint8(8))).Params()
 43+	white := ui.vx.QueryColor(vaxis.IndexColor(uint8(15))).Params()
 44+	if len(bg) == 3 && len(gray) == 3 && reflect.DeepEqual(bg, gray) {
 45+		// Color theme with background set to gray: gray would be invisible.
 46+		if len(black) == 3 && !reflect.DeepEqual(bg, black) {
 47+			// Black is distinct from background: use that as gray.
 48+			ui.config.Colors.Gray = vaxis.IndexColor(0)
 49+		} else if len(white) == 3 && !reflect.DeepEqual(bg, white) {
 50+			// Black == gray == background color. (Can happen in 8-color themes.)
 51+			// Use an RGB color as the gray color, interpolated from black and white.
 52+			p := make([]uint8, 3)
 53+			for i := range p {
 54+				p[i] = uint8((int(black[i]) + int(white[i])) / 2)
 55+			}
 56+			ui.config.Colors.Gray = vaxis.RGBColor(p[0], p[1], p[2])
 57+		} else {
 58+			// Black == gray == background == white. Give up.
 59+			ui.config.Colors.Gray = ColorDefault
 60+		}
 61+	}
 62+	if ui.config.Colors.Status == ColorDefault {
 63+		ui.config.Colors.Status = ui.config.Colors.Gray
 64+	}
 65+
 66 	ui.vx.SetTitle("senpai")
 67 	ui.vx.SetAppID("senpai")
 68 
 69@@ -143,7 +172,7 @@ func New(config Config) (ui *UI, err error) {
 70 	ui.e = NewEditor(ui)
 71 	ui.Resize()
 72 
 73-	return
 74+	return ui, ui.config.Colors, nil
 75 }
 76 
 77 func (ui *UI) ShouldExit() bool {
 78@@ -760,6 +789,20 @@ func (ui *UI) ScrollToBuffer() {
 79 	}
 80 }
 81 
 82+func (ui *UI) drawHorizontalLine(vx *Vaxis, x0, y, width int) {
 83+	for x := x0; x < x0+width; x++ {
 84+		setCell(vx, x, y, '─', vaxis.Style{
 85+			Foreground: ui.config.Colors.Gray,
 86+		})
 87+	}
 88+}
 89+
 90+func (ui *UI) drawVerticalLine(vx *Vaxis, x, y0, height int) {
 91+	for y := y0; y < y0+height; y++ {
 92+		setCell(vx, x, y, '│', vaxis.Style{})
 93+	}
 94+}
 95+
 96 func (ui *UI) drawStatusBar(x0, y, width int) {
 97 	clearArea(ui.vx, x0, y, width, 1)
 98 
 99@@ -769,7 +812,7 @@ func (ui *UI) drawStatusBar(x0, y, width int) {
100 
101 	var s StyledStringBuilder
102 	s.SetStyle(vaxis.Style{
103-		Foreground: ColorGray,
104+		Foreground: ui.config.Colors.Gray,
105 	})
106 	s.WriteString("--")
107 
108@@ -779,7 +822,7 @@ func (ui *UI) drawStatusBar(x0, y, width int) {
109 
110 	s.Reset()
111 	s.SetStyle(vaxis.Style{
112-		Foreground: ColorGray,
113+		Foreground: ui.config.Colors.Gray,
114 	})
115 	s.WriteString(ui.status)
116 
117@@ -787,7 +830,7 @@ func (ui *UI) drawStatusBar(x0, y, width int) {
118 }
119 
120 func (ui *UI) drawVerticalMemberList(vx *Vaxis, x0, y0, width, height int, b *buffer, members []irc.Member, offset *int) {
121-	drawVerticalLine(vx, x0, y0, height)
122+	ui.drawVerticalLine(vx, x0, y0, height)
123 	x0++
124 	width--
125 	clearArea(vx, x0, y0, width, height)
126@@ -797,7 +840,7 @@ func (ui *UI) drawVerticalMemberList(vx *Vaxis, x0, y0, width, height int, b *bu
127 		printString(vx, &x, y0, Styled("Help", vaxis.Style{
128 			Foreground: ui.config.Colors.Status,
129 		}))
130-		drawHorizontalLine(vx, x0, y0+1, width)
131+		ui.drawHorizontalLine(vx, x0, y0+1, width)
132 		y0 += 2
133 
134 		lines := []string{
135@@ -812,7 +855,7 @@ func (ui *UI) drawVerticalMemberList(vx *Vaxis, x0, y0, width, height int, b *bu
136 			}
137 			x := x0
138 			printString(vx, &x, y0, Styled(line, st))
139-			drawHorizontalLine(vx, x0, y0+1, width)
140+			ui.drawHorizontalLine(vx, x0, y0+1, width)
141 			y0 += 2
142 		}
143 		return
144@@ -837,7 +880,7 @@ func (ui *UI) drawVerticalMemberList(vx *Vaxis, x0, y0, width, height int, b *bu
145 	}
146 	y0++
147 	height--
148-	drawHorizontalLine(vx, x0, y0, width)
149+	ui.drawHorizontalLine(vx, x0, y0, width)
150 	y0++
151 	height--
152 
153@@ -855,7 +898,7 @@ func (ui *UI) drawVerticalMemberList(vx *Vaxis, x0, y0, width, height int, b *bu
154 	actions = append(actions, "→ Leave")
155 	for i, action := range actions {
156 		x := x0
157-		drawHorizontalLine(vx, x0, y0+height-(len(actions)-i)*2, width)
158+		ui.drawHorizontalLine(vx, x0, y0+height-(len(actions)-i)*2, width)
159 		printString(vx, &x, y0+height-(len(actions)-i)*2+1, Styled(action, vaxis.Style{}))
160 	}
161 	height -= 2 * len(actions)
162@@ -895,7 +938,7 @@ func (ui *UI) drawVerticalMemberList(vx *Vaxis, x0, y0, width, height int, b *bu
163 			x += padding - 1
164 			powerLevelText := m.PowerLevel[:1]
165 			powerLevelSt := vaxis.Style{
166-				Foreground: vaxis.IndexColor(2),
167+				Foreground: ColorGreen,
168 				Attribute:  attr,
169 			}
170 			printString(vx, &x, y, Styled(powerLevelText, powerLevelSt))
171@@ -907,7 +950,7 @@ func (ui *UI) drawVerticalMemberList(vx *Vaxis, x0, y0, width, height int, b *bu
172 		nameText := truncate(vx, m.Name.Name, width-1, "\u2026")
173 		if m.Away {
174 			name = Styled(nameText, vaxis.Style{
175-				Foreground: ColorGray,
176+				Foreground: ui.config.Colors.Gray,
177 				Attribute:  attr,
178 			})
179 		} else {