Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243package tui
import ( "fmt" "math" "os" "regexp" "strconv")
// ink is the transcript's brightness ramp. Each step is mixed from the// terminal's own text and background colors to a contrast target, so the// ramp keeps its spacing on any theme instead of trusting palette slots.type ink struct { // code steps down from prose for inline and fenced code. code string // secondary is text read at a glance: tool rows, timings, notices. secondary string // decor is structure that is seen, not read: list markers, gutters. decor string // busy is albedo's rail color while it works in tools. busy string // codeBg tints the cells under inline code, so a span keeps prose // contrast and still reads as code. codeBg string // surface lifts a selected row off the background. surface string // added and removed are the diff row tints. added, removed string // brandFrom and brandTo are the ends of the brand gradient. brandFrom, brandTo string}
// APCA contrast targets (Lc) for each step: about 60 suits content text,// 45 text read at a glance, and 15 to 30 marks that are not read.// The code tint is a luminance step from the background, measured as a// contrast ratio with the flare term so a black background gets a larger lift// than a gray one. Dark themes tint toward the palette's blue, light themes// step toward the text, since a blue wash on a light background reads as a// marker highlight.const ( darkCodeBgRatio = 1.3 lightCodeBgRatio = 1.65 surfaceRatio = 1.25 diffRatio = 1.35)
const ( codeLc = 64 secondaryLc = 45 decorLc = 22 busyLc = 30)
type rgb struct{ r, g, b float64 }
func (c rgb) hex() string { b := func(v float64) int { return int(math.Round(math.Max(0, math.Min(1, v)) * 255)) } return "#" + strconv.FormatInt(int64(1<<24|b(c.r)<<16|b(c.g)<<8|b(c.b)), 16)[1:]}
func (c rgb) mix(to rgb, t float64) rgb { return rgb{c.r + (to.r-c.r)*t, c.g + (to.g-c.g)*t, c.b + (to.b-c.b)*t}}
// apca is the magnitude of the APCA 0.0.98G lightness contrast of text on bg.func apca(text, bg rgb) float64 { y := func(c rgb) float64 { v := 0.2126729*math.Pow(c.r, 2.4) + 0.7151522*math.Pow(c.g, 2.4) + 0.0721750*math.Pow(c.b, 2.4) if v < 0.022 { v += math.Pow(0.022-v, 1.414) } return v } yt, yb := y(text), y(bg) var s float64 if yb > yt { s = (math.Pow(yb, 0.56) - math.Pow(yt, 0.57)) * 1.14 } else { s = (math.Pow(yb, 0.65) - math.Pow(yt, 0.62)) * 1.14 } if math.Abs(s) < 0.1 { return 0 } return (math.Abs(s) - 0.027) * 100}
// toward mixes from into bg until it reaches the target contrast. A color// already under the target stays as it is.func toward(from, bg rgb, target float64) rgb { if apca(from, bg) <= target { return from } lo, hi := 0.0, 1.0 for range 24 { mid := (lo + hi) / 2 if apca(from.mix(bg, mid), bg) > target { lo = mid } else { hi = mid } } return from.mix(bg, lo)}
// luminance is relative luminance as the WCAG contrast ratio defines it.func luminance(c rgb) float64 { lin := func(v float64) float64 { if v <= 0.04045 { return v / 12.92 } return math.Pow((v+0.055)/1.055, 2.4) } return 0.2126*lin(c.r) + 0.7152*lin(c.g) + 0.0722*lin(c.b)}
// ratio is the WCAG contrast ratio between two colors.func ratio(a, b rgb) float64 { la, lb := luminance(a)+0.05, luminance(b)+0.05 return max(la, lb) / min(la, lb)}
// step mixes bg toward to until the two differ by the given contrast ratio,// or reaches to when it cannot.func step(bg, to rgb, target float64) rgb { if ratio(bg, to) <= target { return to } lo, hi := 0.0, 1.0 for range 24 { mid := (lo + hi) / 2 if ratio(bg, bg.mix(to, mid)) < target { lo = mid } else { hi = mid } } return bg.mix(to, hi)}
// termColors is what the terminal reported. A missing color is nil, and// palette holds only the entries that were reported.type termColors struct { fg, bg *rgb palette map[int]rgb}
// The palette entries the theme mixes from.const ( ansiRed = 1 ansiGreen = 2 ansiBlue = 4 ansiMagenta = 5 ansiCyan = 6)
var queriedPalette = []int{ansiRed, ansiGreen, ansiBlue, ansiMagenta, ansiCyan}
// minTrustLc is the least contrast a reported text color may have with the// reported background. Below it the reply describes something other than// what is on screen, like a multiplexer that answers with black for colors// it does not know, and the palette fallbacks are safer.const minTrustLc = 60
func mixInk(c termColors) (ink, bool) { if c.fg == nil || c.bg == nil || apca(*c.fg, *c.bg) < minTrustLc { return ink{}, false } fg, bg := *c.fg, *c.bg out := ink{ code: toward(fg, bg, codeLc).hex(), secondary: toward(fg, bg, secondaryLc).hex(), decor: toward(fg, bg, decorLc).hex(), } out.surface = step(bg, fg, surfaceRatio).hex() blue, hasBlue := c.palette[ansiBlue] switch { case luminance(bg) > luminance(fg): out.codeBg = step(bg, fg, lightCodeBgRatio).hex() case hasBlue: out.codeBg = step(bg, blue, darkCodeBgRatio).hex() default: out.codeBg = step(bg, fg, darkCodeBgRatio).hex() } if magenta, ok := c.palette[ansiMagenta]; ok && apca(magenta, bg) >= busyLc { out.busy = toward(magenta, bg, busyLc).hex() } if red, ok := c.palette[ansiRed]; ok { out.removed = step(bg, red, diffRatio).hex() } if green, ok := c.palette[ansiGreen]; ok { out.added = step(bg, green, diffRatio).hex() } // the brand runs from your color toward violet, halfway between the // palette's blue and magenta cyan, hasCyan := c.palette[ansiCyan] magenta, hasMagenta := c.palette[ansiMagenta] if hasCyan && hasBlue && hasMagenta { out.brandFrom, out.brandTo = cyan.hex(), blue.mix(magenta, 0.5).hex() } return out, true}
var oscColor = regexp.MustCompile(`\x1b\](10|11|4;(\d+));rgb:([0-9a-fA-F]{1,4})/([0-9a-fA-F]{1,4})/([0-9a-fA-F]{1,4})`)
// parseColors reads OSC 10, 11 and 4 replies, whose channels carry one to// four hex digits each.func parseColors(reply string) termColors { c := termColors{palette: map[int]rgb{}} for _, m := range oscColor.FindAllStringSubmatch(reply, -1) { channel := func(h string) float64 { v, _ := strconv.ParseUint(h, 16, 16) return float64(v) / float64(uint64(1)<<(4*len(h))-1) } col := &rgb{channel(m[3]), channel(m[4]), channel(m[5])} switch m[1] { case "10": c.fg = col case "11": c.bg = col default: index, _ := strconv.Atoi(m[2]) c.palette[index] = *col } } return c}
var transcriptInk ink
// DetectInk asks the terminal for its colors and mixes the transcript's// ramp from them. It must run before the TUI owns the terminal's input.func DetectInk() { reply := queryColors() detected, ok := mixInk(parseColors(reply)) if path := os.Getenv("ALBEDO_INK_DEBUG"); path != "" { _ = os.WriteFile(path, fmt.Appendf(nil, "reply %q\ntrusted %v\nink %+v\n", reply, ok, detected), 0o644) } if ok { useInk(detected) }}