Replacing a color with the palette entry nearest in lightness turned muted text red and body text teal on some schemes; moving it toward the scheme's text color keeps grays gray and hues close. Config warnings name files relative to the config directory, and paths in notes use ~.
- commit
- 7e2d997efe21c40a1aa5fc8717a73e805e928807
- parent
- 446a78d920b98bb9e1ff565a32f4dd2a72230f02
- author
- Dami <dami.etoile@gmail.com>
| M | cmd/ | 4 | +- |
| M | internal/ | 15 | +++++++++ |
| M | internal/ | 13 | +++++++ |
| M | internal/ | 4 | +- |
| M | internal/ | 52 | ++++++++++++++---------------- |
| M | internal/ | 10 | +++--- |
| M | internal/ | 2 | +- |
@@ -109,9 +109,9 @@ func configNotes(c *config.Config) []string {
109109 case 0:110110 return nil111111 case 1:112- return []string{"Config: " + c.Warnings[0].String()}112+ return []string{"Config: " + c.Warnings[0].Short()}113113 default:114- return []string{fmt.Sprintf("Config: %s (and %d more; run 012 config)", c.Warnings[0], n-1)}114+ return []string{fmt.Sprintf("Config: %s (and %d more; run 012 config)", c.Warnings[0].Short(), n-1)}115115 }116116 }117117 @@ -63,6 +63,21 @@ func (w Warning) String() string {
6363 return w.Src.String() + ": " + w.Msg6464 }6565 66+// Short is the warning with a file in the config directory named67+// relative to it ("config:3: ..."), to fit on the context line.68+func (w Warning) Short() string {69+ if w.Src.Kind == FromFile {70+ if dir, err := Dir(); err == nil {71+ if rel, err := filepath.Rel(dir, w.Src.Name); err == nil && !strings.HasPrefix(rel, "..") {72+ s := w.Src73+ s.Name = rel74+ return Warning{Src: s, Msg: w.Msg}.String()75+ }76+ }77+ }78+ return w.String()79+}80+6681 // Config is the effective configuration.6782 type Config struct {6883 // Path is the main config file, which may not exist.@@ -7,6 +7,7 @@ import (
77 "os/exec"88 "path/filepath"99 "runtime"10+ "strings"1011 )1112 1213 // Editor returns the command that opens path in the user's editor:@@ -41,3 +42,15 @@ func EnsureFile(path string) error {
4142 }4243 return os.WriteFile(path, []byte(DefaultFile()), 0o600)4344 }45+46+// Tilde shortens a path in the home directory to ~/..., for messages.47+func Tilde(path string) string {48+ home, err := os.UserHomeDir()49+ if err != nil || home == "" {50+ return path51+ }52+ if rel, err := filepath.Rel(home, path); err == nil && rel != "." && !strings.HasPrefix(rel, "..") {53+ return "~" + string(filepath.Separator) + rel54+ }55+ return path56+}@@ -119,7 +119,7 @@ func (m *Model) reloadConfig() {
119119 m.prefs.Config = c120120 var problems []string121121 for _, w := range c.Warnings {122- problems = append(problems, w.String())122+ problems = append(problems, w.Short())123123 }124124 if p := m.applyConfig(); p != "" {125125 problems = append(problems, p)@@ -128,7 +128,7 @@ func (m *Model) reloadConfig() {
128128 m.note = m.th.Warning.Render("Config reloaded with problems: " + strings.Join(problems, "; "))129129 return130130 }131- m.note = "Config reloaded from " + c.Path131+ m.note = "Config reloaded from " + config.Tilde(c.Path)132132 }133133 134134 // keySavedMsg reports storing the API key, and the client made with it.@@ -6,9 +6,8 @@ import (
66 )77 88 // Contrast correction for color schemes, adapted from puzzletea's theme9-// package (same author): WCAG 2 contrast, and a readable replacement for10-// a foreground that falls short, taken from the scheme's own colors when11-// one qualifies.9+// package (same author): WCAG 2 contrast, and a foreground that falls10+// short moved just far enough to be readable.1211 1312 // Contrast minimums.1413 const (@@ -39,27 +38,35 @@ func contrast(a, b color.Color) float64 {
3938 return (la + 0.05) / (lb + 0.05)4039 }4140 42-// readable returns fg, or when it has less than min contrast with bg,43-// the scheme color closest to it in lightness that has enough, or failing44-// that the nearest color toward black or white that does.45-func readable(fg, bg color.Color, p *Palette, min float64) color.Color {41+// readable returns fg, or when it has less than min contrast with bg, fg42+// moved just far enough toward toward (the scheme's text color, which43+// keeps grays gray and hues close) to reach it, or failing that toward44+// black or white.45+func readable(fg, bg, toward color.Color, min float64) color.Color {4646 if contrast(fg, bg) >= min {4747 return fg4848 }49- target := luminance(fg)50- var best color.Color51- bestDist := math.Inf(1)52- for _, c := range append(p.ANSI[:], p.Foreground, p.Background) {53- if d := math.Abs(luminance(c) - target); contrast(c, bg) >= min && d < bestDist {54- best, bestDist = c, d55- }56- }57- if best != nil {58- return best49+ if toward != nil && contrast(toward, bg) >= min {50+ return blendUntil(fg, toward, bg, min)5951 }6052 return nudge(fg, bg, min)6153 }6254 55+// blendUntil moves fg toward end until it has min contrast with bg; end56+// itself must have it.57+func blendUntil(fg, end, bg color.Color, min float64) color.Color {58+ lo, hi := 0.0, 1.059+ for range 24 {60+ mid := (lo + hi) / 261+ if contrast(blend(fg, end, mid), bg) >= min {62+ hi = mid63+ } else {64+ lo = mid65+ }66+ }67+ return blend(fg, end, hi)68+}69+6370 // nudge blends fg toward white or black, whichever moves it away from6471 // bg, just far enough to reach min contrast.6572 func nudge(fg, bg color.Color, min float64) color.Color {@@ -74,16 +81,7 @@ func nudge(fg, bg color.Color, min float64) color.Color {
7481 end = color.White7582 }7683 }77- lo, hi := 0.0, 1.078- for range 24 {79- mid := (lo + hi) / 280- if contrast(blend(fg, end, mid), bg) >= min {81- hi = mid82- } else {83- lo = mid84- }85- }86- return blend(fg, end, hi)84+ return blendUntil(fg, end, bg, min)8785 }8886 8987 // distinct returns bg, or when it's too close to the screen to show, bg@@ -24,12 +24,12 @@ const barBlend = 0.12
2424 // - a background role too close to the screen's is moved apart;2525 // - text that falls short of its contrast minimum (4.5:1 for text, 3:12626 // for hints and lines, 2:1 for unavailable items) against its27-// background is replaced by the scheme color nearest it in lightness28-// that meets it, or nudged toward black or white.27+// background is moved toward the scheme's text color just far enough28+// to meet it, or toward black or white.2929 func FromPalette(p Palette) Theme {3030 t := New(p.Dark)3131 t.Name, t.Palette = p.Name, &p32- text := readable(p.Foreground, p.Background, &p, minText)32+ text := readable(p.Foreground, p.Background, nil, minText)3333 t.Screen = lipgloss.NewStyle().Background(p.Background).Foreground(text)3434 eachRole(&t, func(_ string, s *lipgloss.Style) { *s = mapStyle(*s, &p) })3535 if p.Selection != nil && contrast(p.Selection, p.Background) >= minDistinct {@@ -119,11 +119,11 @@ func fixContrast(role string, s lipgloss.Style, text color.Color, p *Palette) li
119119 }120120 }121121 if c := s.GetForeground(); !isNoColor(c) && role != "SeriesBg" {122- s = s.Foreground(readable(c, bg, p, minContrast(role)))122+ s = s.Foreground(readable(c, bg, text, minContrast(role)))123123 } else if !isNoColor(s.GetBackground()) && role != "SeriesBg" {124124 // Text on a background role with no color of its own is drawn in125125 // the screen's text color.126- s = s.Foreground(readable(text, bg, p, minContrast(role)))126+ s = s.Foreground(readable(text, bg, nil, minContrast(role)))127127 }128128 return s129129 }@@ -131,7 +131,7 @@ func (m *Model) keepTheme(name string) {
131131 m.fail("Couldn't save the theme: " + err.Error())132132 return133133 }134- m.note = "Theme " + name + ", saved in " + c.Path134+ m.note = "Theme " + name + ", saved in " + config.Tilde(c.Path)135135 if was.Kind == config.FromEnv || was.Kind == config.FromFlag {136136 m.note += " (" + was.String() + " overrides it at startup)"137137 }