diff options
| author | historia <[not public]> | 2026-07-15 23:44:32 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-15 23:44:32 -0400 |
| commit | ecf4c872bb33381abc065f1816e204befcde610c (patch) | |
| tree | 65e08a55bd577fcba6d714a13124b4912443be08 | |
| parent | 1068432d94252b9b4dec447e424f85f712fa16c2 (diff) | |
| download | xterm256-color-palette-ecf4c872bb33381abc065f1816e204befcde610c.tar.gz | |
initial commit
| -rw-r--r-- | README.md | 27 | ||||
| -rw-r--r-- | chart.go | 507 | ||||
| -rw-r--r-- | color/color.go | 103 | ||||
| -rw-r--r-- | go.mod | 7 | ||||
| -rw-r--r-- | go.sum | 4 |
5 files changed, 619 insertions, 29 deletions
@@ -1,4 +1,25 @@ -# xterm-color-chart -XTerm 256 color chart +# xterm265-color-palette -
\ No newline at end of file +A TUI XTerm 256 color palette tool. This is just a small fork of [xterm-color-chart](https://github.com/kutuluk/xterm-color-chart) by Evgeniy Pavlov. + +Click on colors to select them and build a palette. +`h` to cycle harmony modes +`<Space>` to make a palette based on the first color +`e` to try to extend the palette with similar colors +`t` to toggle the 16 system colors as palette seeds/outputs (off by default; they depend on your terminal theme) +`q` to quit + +## Build + +Requires [Go](https://go.dev/). + +```sh +git clone https://codeberg.org/historia/xterm256-color-palette +cd xterm256-color-palette +go build -o xtp . +./xtp +``` + +## License + +MIT @@ -3,8 +3,10 @@ package main import ( "fmt" "math" + "math/rand" + "time" - "github.com/kutuluk/xterm-color-chart/color" + "codeberg.org/historia/xterm256-color-palette/color" "github.com/nsf/termbox-go" ) @@ -44,6 +46,196 @@ func termNative(c int) termbox.Attribute { return termbox.Attribute(c + 1) } +type hitRegion struct { + x, y, w, h int + leftAction func(x, y int) + rightAction func(x, y int) +} + +var hitRegions []hitRegion + +func addHitRegion(x, y, w, h int, leftAction, rightAction func(x, y int)) { + hitRegions = append(hitRegions, hitRegion{x, y, w, h, leftAction, rightAction}) +} + +func hitTest(px, py int) *hitRegion { + for i := len(hitRegions) - 1; i >= 0; i-- { + r := &hitRegions[i] + if px >= r.x && px < r.x+r.w && py >= r.y && py < r.y+r.h { + return r + } + } + return nil +} + +var selectedColors []int +var paletteScroll int +var autoScrollPalette bool +var paletteLeft int +var paletteTop int +var lastMouseEvent time.Time +var dragIndex = -1 +var dragY int + +func addSelectedColor(c int) { + for _, sc := range selectedColors { + if sc == c { + return + } + } + selectedColors = append(selectedColors, c) + autoScrollPalette = true +} + +func removeSelectedColor(i int) { + if i >= 0 && i < len(selectedColors) { + selectedColors = append(selectedColors[:i], selectedColors[i+1:]...) + } + if paletteScroll > len(selectedColors)-1 { + paletteScroll = len(selectedColors) - 1 + } + if paletteScroll < 0 { + paletteScroll = 0 + } +} + +func moveSelectedColor(fromIdx, toIdx int) { + if fromIdx < 0 || fromIdx >= len(selectedColors) || toIdx < 0 || toIdx >= len(selectedColors) || fromIdx == toIdx { + return + } + c := selectedColors[fromIdx] + selectedColors = append(selectedColors[:fromIdx], selectedColors[fromIdx+1:]...) + selectedColors = append(selectedColors[:toIdx], append([]int{c}, selectedColors[toIdx:]...)...) +} + +var harmonies = []struct { + name string + offsets []float64 +}{ + {"complementary", []float64{0, 180}}, + {"analogous", []float64{0, 330, 30}}, + {"triadic", []float64{0, 120, 240}}, + {"split-comp", []float64{0, 150, 210}}, + {"tetradic", []float64{0, 60, 180, 240}}, + {"square", []float64{0, 90, 180, 270}}, + {"monochromatic", nil}, +} + +var currentHarmony = 2 // triadic + +// включать ли 16 системных цветов терминала в генерацию палитр +// (их реальный вид зависит от темы терминала) +var useSystemColors bool + +// привязка LCh-цвета к палитре xterm с уменьшением насыщенности +// до попадания в sRGB-охват (сохраняя светлоту и тон) +func snapColor(lch color.LChColor) int { + for lch.C > 0.5 && !color.InGamut(lch) { + lch.C *= 0.95 + } + var col int + if useSystemColors { + _, col = color.ApproximateAll(lch, comparers[currentComparer]) + } else { + _, col = color.Approximate(lch, comparers[currentComparer]) + } + return col +} + +// генерация гармоничной палитры в пространстве LCh +func generateHarmony() { + har := harmonies[currentHarmony] + + var base color.LChColor + seeded := false + if len(selectedColors) > 0 { + c := selectedColors[0] + if useSystemColors || c-1 >= 16 { + base = color.TermLab(c).LCh() + seeded = true + } + } + if !seeded { + rnd := color.LChColor{ + L: 40.0 + 40.0*rand.Float64(), + C: 40.0 + 50.0*rand.Float64(), + H: 360.0 * rand.Float64(), + } + // привязка к ближайшему цвету xterm, чтобы повторная генерация давала тот же результат + c := snapColor(rnd) + base = color.TermLab(c).LCh() + } + + selectedColors = selectedColors[:0] + paletteScroll = 0 + + if har.offsets == nil { + // монохроматическая: один тон, разная светлота + for i := 0; i < 5; i++ { + l := 20.0 + 15.0*float64(i) + addSelectedColor(snapColor(color.LChColor{L: l, C: base.C, H: base.H})) + } + } else { + for _, off := range har.offsets { + hue := math.Mod(base.H+off, 360.0) + addSelectedColor(snapColor(color.LChColor{L: base.L, C: base.C, H: hue})) + } + } +} + +// расширение палитры: для каждого цвета добавляются тень, блик и приглушенный +// вариант того же тона, сгруппированные сразу после исходного цвета +func expandPalette() { + if len(selectedColors) == 0 { + return + } + + clampL := func(l float64) float64 { + if l < 10.0 { + return 10.0 + } + if l > 90.0 { + return 90.0 + } + return l + } + + expanded := make([]int, 0, len(selectedColors)*4) + contains := func(c int) bool { + for _, sc := range expanded { + if sc == c { + return true + } + } + return false + } + + for _, c := range selectedColors { + if !contains(c) { + expanded = append(expanded, c) + } + if !useSystemColors && c-1 < 16 { + continue + } + lch := color.TermLab(c).LCh() + variants := []color.LChColor{ + {L: clampL(lch.L - 25.0), C: lch.C, H: lch.H}, + {L: clampL(lch.L + 25.0), C: lch.C, H: lch.H}, + {L: lch.L, C: lch.C * 0.5, H: lch.H}, + } + for _, v := range variants { + col := snapColor(v) + if !contains(col) { + expanded = append(expanded, col) + } + } + } + + selectedColors = expanded + paletteScroll = 0 + autoScrollPalette = false +} + func printString(x, y int, s string) { // TODO срабатывает перенос на следующую строку при достижении правого края // и ошибка при выходе из границ буфера снизу @@ -112,6 +304,13 @@ func DrawColor(x, y int, c int) { printLineAttr(x+1, y, fmt.Sprintf("%03d #%s", int(c), color.XtermRGBPalette[c]), fg, termbox.Attribute(c)) } +var ( + boxCacheSize int + boxCacheLight int + boxCacheComp int + boxCacheBlocks []struct{ fg, bg termbox.Attribute } +) + func DrawBox(left, top, d, l int) { var approxer string switch currentComparer { @@ -123,30 +322,44 @@ func DrawBox(left, top, d, l int) { approxer = "CIE2000" } - r := d / 2 - for y := 0; y < d; y++ { - for x := 0; x < d; x++ { - a := 100.0 * float64(x-r) / float64(r) - b := -100.0 * float64(y-r) / float64(r) - _, c := color.Approximate(color.LabColor{float64(l), a, b}, comparers[currentComparer]) - //termbox.SetCell(x*2+left, y+top, ' ', background, termbox.Attribute(color)) - //termbox.SetCell(x*2+left+1, y+top, ' ', background, termbox.Attribute(color)) - fg := int(background) - if y == 1 || y == 2 || y == d-2 { - fgLab := color.XtermRGBPalette[c-17].Lab() - if l > 50 { - fgLab.L -= 50 - //fgLab.L = 20 - } else { - fgLab.L += 50 - //fgLab.L = 70 + hit := d == boxCacheSize && l == boxCacheLight && currentComparer == boxCacheComp && boxCacheBlocks != nil + + if hit { + idx := 0 + for y := 0; y < d; y++ { + for x := 0; x < d; x++ { + blk := boxCacheBlocks[idx] + termbox.SetCell(x*2+left, y+top, ' ', blk.fg, blk.bg) + termbox.SetCell(x*2+left+1, y+top, ' ', blk.fg, blk.bg) + idx++ + } + } + } else { + r := d / 2 + boxCacheBlocks = make([]struct{ fg, bg termbox.Attribute }, d*d) + idx := 0 + for y := 0; y < d; y++ { + for x := 0; x < d; x++ { + a := 100.0 * float64(x-r) / float64(r) + b := -100.0 * float64(y-r) / float64(r) + _, c := color.Approximate(color.LabColor{float64(l), a, b}, comparers[currentComparer]) + fg := int(background) + if y == 1 || y == 2 || y == d-2 { + fgLab := color.TermLab(c) + if l > 50 { + fgLab.L -= 50 + } else { + fgLab.L += 50 + } + _, fg = color.Approximate(fgLab, color.DeltaCIE2000{}) } - _, fg = color.Approximate(fgLab, color.DeltaCIE2000{}) + boxCacheBlocks[idx] = struct{ fg, bg termbox.Attribute }{termbox.Attribute(fg), termbox.Attribute(c)} + termbox.SetCell(x*2+left, y+top, ' ', termbox.Attribute(fg), termbox.Attribute(c)) + termbox.SetCell(x*2+left+1, y+top, ' ', termbox.Attribute(fg), termbox.Attribute(c)) + idx++ } - termbox.SetCell(x*2+left, y+top, ' ', termbox.Attribute(fg), termbox.Attribute(c)) - termbox.SetCell(x*2+left+1, y+top, ' ', termbox.Attribute(fg), termbox.Attribute(c)) } - + boxCacheSize, boxCacheLight, boxCacheComp = d, l, currentComparer } labLabel := "CIE L*a*b color space" @@ -212,6 +425,10 @@ func DrawCircle(left, top, r int, l float64) { func DrawColorCell(x, y, c int) { termbox.SetCell(x, y, ' ', termbox.Attribute(1), termbox.Attribute(c)) termbox.SetCell(x+1, y, ' ', termbox.Attribute(1), termbox.Attribute(c)) + val := c + addHitRegion(x, y, 2, 1, func(_, _ int) { + addSelectedColor(val) + }, nil) } // left, top - левый верхний угол отрисовки палитры @@ -286,6 +503,108 @@ func Fill(x1, y1, x2, y2 int, fg, bg termbox.Attribute) { } } +func drawPaletteEntry(x, y, c, idx int) { + termbox.SetCell(x, y, ' ', termbox.Attribute(1), termbox.Attribute(c)) + termbox.SetCell(x+1, y, ' ', termbox.Attribute(1), termbox.Attribute(c)) + label := fmt.Sprintf("%03d / %02X", c-1, c-1) + if dragIndex == idx { + printLineAttr(x+3, y, label, background, background+7) + } else { + printLineAttr(x+3, y, label, text, background) + } + addHitRegion(x, y, 3+len(label), 1, + func(_, _ int) { + if dragIndex < 0 { + dragIndex = idx + } + }, + func(_, _ int) { + removeSelectedColor(idx) + if dragIndex == idx { + dragIndex = -1 + } else if dragIndex > idx { + dragIndex-- + } + }) +} + +// панель пользовательской палитры +func DrawUserPalette(left, top int) { + paletteLeft = left + paletteTop = top + + printLineAttr(left, top, fmt.Sprintf("Palette (%d) [%s]", len(selectedColors), harmonies[currentHarmony].name), text, background) + + n := len(selectedColors) + if n == 0 { + paletteScroll = 0 + autoScrollPalette = false + printLineAttr(left, top+2, "Click a color to add it here,", background+6, background) + printLineAttr(left, top+3, "or press Space to generate", background+6, background) + return + } + + rows := screen.Height - (top + 2) + if rows <= 0 { + autoScrollPalette = false + return + } + + if n <= rows { + paletteScroll = 0 + autoScrollPalette = false + for i, c := range selectedColors { + drawPaletteEntry(left, top+2+i, c, i) + } + return + } + + maxScroll := n - (rows - 1) + if maxScroll > n-1 { + maxScroll = n - 1 + } + if autoScrollPalette { + paletteScroll = maxScroll + autoScrollPalette = false + } + if paletteScroll > maxScroll { + paletteScroll = maxScroll + } + if paletteScroll < 0 { + paletteScroll = 0 + } + + visible := rows + if paletteScroll > 0 { + visible-- + } + if paletteScroll+visible < n { + visible-- + } + if visible < 1 { + visible = 1 + } + + y := top + 2 + if paletteScroll > 0 { + printLineAttr(left, y, fmt.Sprintf("^ %d more", paletteScroll), background+7, background) + y++ + } + + end := paletteScroll + visible + if end > n { + end = n + } + for i := paletteScroll; i < end; i++ { + drawPaletteEntry(left, y, selectedColors[i], i) + y++ + } + + if below := n - end; below > 0 && y < screen.Height { + printLineAttr(left, y, fmt.Sprintf("v %d more", below), background+7, background) + } +} + var ( text, background termbox.Attribute lightness int @@ -293,6 +612,7 @@ var ( ) func DrawChart() { + hitRegions = hitRegions[:0] screen.Clear(text, background) //Fill(0, 0, screen.Width-1, screen.Height-1, text, background) @@ -313,6 +633,27 @@ func DrawChart() { DrawBox(2, 2, boxSize, lightness) // http://snag.gy/XMro8.jpg - картинка для сравнения + boxLeft, boxTop := 2, 2 + boxW, boxH := boxSize*2, boxSize + bs := boxSize + lt := lightness + addHitRegion(boxLeft, boxTop, boxW, boxH, func(px, py int) { + cx := (px - boxLeft) / 2 + cy := py - boxTop + r := bs / 2 + if r > 0 && cx >= 0 && cx < bs && cy >= 0 && cy < bs { + a := 100.0 * float64(cx-r) / float64(r) + b := -100.0 * float64(cy-r) / float64(r) + _, col := color.Approximate(color.LabColor{float64(lt), a, b}, comparers[currentComparer]) + addSelectedColor(col) + } + }, nil) + + sysState := "off" + if useSystemColors { + sysState = "on" + } + helpText := []string{ "Control keys:", "", @@ -324,6 +665,22 @@ func DrawChart() { "CIE94 - middle", "CIE2000 - slowest", "", + "Click a color to add it to the palette", + "L-click palette entry to pick up / drop", + "R-click palette entry to remove", + "Backspace - remove last palette entry", + "Space - generate harmony palette", + " (seeds from first entry if any)", + "h - change harmony mode", + "e - expand palette with similar colors", + "t - use 16 system colors in generated", + fmt.Sprintf(" palettes, as seeds and outputs (%s).", sysState), + " When off, a system color seed is", + " replaced by a random one. Note: system", + " colors depend on your terminal theme,", + " so results assume xterm defaults", + "q/Esc/F10 - quit", + "Wheel/PgUp/PgDn - scroll palette", "", } @@ -341,6 +698,8 @@ func DrawChart() { DrawPalette(xc, yc, 3, true) } + DrawUserPalette(xc, yc+3*7+11) + screen.Flush() } @@ -355,7 +714,7 @@ func main() { } defer termbox.Close() termbox.SetOutputMode(termbox.Output256) - //termbox.SetInputMode(termbox.InputEsc) + termbox.SetInputMode(termbox.InputEsc | termbox.InputMouse) background = termNative(234) text = background + 12 @@ -374,7 +733,16 @@ func main() { switch ev.Key { case termbox.KeyCtrlQ, termbox.KeyF10: loop = false - case termbox.KeyCtrlH, termbox.KeyF1: + case termbox.KeyEsc: + if time.Since(lastMouseEvent) > 150*time.Millisecond { + loop = false + } + case termbox.KeyBackspace, termbox.KeyBackspace2: + if len(selectedColors) > 0 { + removeSelectedColor(len(selectedColors) - 1) + DrawChart() + } + case termbox.KeyF1: helpVisible = !helpVisible DrawChart() case termbox.KeyArrowUp: @@ -401,6 +769,97 @@ func main() { currentComparer = 0 } DrawChart() + case termbox.KeySpace: + generateHarmony() + DrawChart() + case termbox.KeyPgup: + if paletteScroll > 0 { + paletteScroll-- + DrawChart() + } + case termbox.KeyPgdn: + paletteScroll++ + DrawChart() + default: + switch ev.Ch { + case 'q', 'Q': + if time.Since(lastMouseEvent) > 150*time.Millisecond { + loop = false + } + case 'h', 'H': + currentHarmony = (currentHarmony + 1) % len(harmonies) + DrawChart() + case 'e', 'E': + expandPalette() + DrawChart() + case 't', 'T': + useSystemColors = !useSystemColors + DrawChart() + } + } + case termbox.EventMouse: + lastMouseEvent = time.Now() + switch ev.Key { + case termbox.MouseLeft: + if dragIndex < 0 { + if r := hitTest(ev.MouseX, ev.MouseY); r != nil && r.leftAction != nil { + r.leftAction(ev.MouseX, ev.MouseY) + } + } + dragY = ev.MouseY + if dragIndex >= 0 { + target := func() int { + n := len(selectedColors) + rows := screen.Height - (paletteTop + 2) + if n <= rows { + t := dragY - (paletteTop + 2) + if t < 0 { + t = 0 + } + if t >= n { + t = n - 1 + } + return t + } + firstEntryY := paletteTop + 2 + if paletteScroll > 0 { + firstEntryY++ + } + t := paletteScroll + (dragY - firstEntryY) + if t < 0 { + t = 0 + } + if t >= n { + t = n - 1 + } + return t + }() + if target != dragIndex { + moveSelectedColor(dragIndex, target) + dragIndex = target + } + } + DrawChart() + case termbox.MouseRight: + if r := hitTest(ev.MouseX, ev.MouseY); r != nil && r.rightAction != nil { + r.rightAction(ev.MouseX, ev.MouseY) + DrawChart() + } + case termbox.MouseRelease: + if dragIndex >= 0 { + dragIndex = -1 + DrawChart() + } + case termbox.MouseWheelUp: + if ev.MouseX >= paletteLeft && paletteScroll > 0 { + paletteScroll-- + DrawChart() + } + case termbox.MouseWheelDown: + if ev.MouseX >= paletteLeft && len(selectedColors) > 0 { + paletteScroll++ + DrawChart() + } } case termbox.EventResize: DrawChart() diff --git a/color/color.go b/color/color.go index a3cbd16..243869b 100644 --- a/color/color.go +++ b/color/color.go @@ -99,9 +99,52 @@ func (xyz XYZColor) String() string { return fmt.Sprintf("XYZ:{%4.7f %4.7f %4.7f}", xyz.X, xyz.Y, xyz.Z) } +// линейные RGB-компоненты (без гамма-коррекции и ограничения диапазона) +func (xyz XYZColor) rgbLinear() (float64, float64, float64) { + // + // [R] [ 3.2406 -1.5372 -0.4986] [X] + // [G] = [-0.9689 1.8758 0.0415] x [Y] + // [B] [ 0.0557 -0.2040 1.0570] [Z] + // + // Observer. = 2°, Illuminant = D65 + r := xyz.X*3.2406 + xyz.Y*-1.5372 + xyz.Z*-0.4986 + g := xyz.X*-0.9689 + xyz.Y*1.8758 + xyz.Z*0.0415 + b := xyz.X*0.0557 + xyz.Y*-0.2040 + xyz.Z*1.0570 + return r, g, b +} + func (xyz XYZColor) RGB() RGBColor { - // TODO!!! - return RGBColor{0, 0, 0} + + f := func(n float64) int { + if n > 0.0031308 { + n = 1.055*math.Pow(n, 1.0/2.4) - 0.055 + } else { + n = n * 12.92 + } + v := int(math.Round(n * 255.0)) + if v < 0 { + v = 0 + } + if v > 255 { + v = 255 + } + return v + } + + r, g, b := xyz.rgbLinear() + return RGBColor{f(r), f(g), f(b)} +} + +// InGamut сообщает, представим ли цвет в sRGB +func InGamut(c Colorer) bool { + const tolerance = 1e-4 + r, g, b := c.XYZ().rgbLinear() + for _, n := range [3]float64{r, g, b} { + if n < -tolerance || n > 1.0+tolerance { + return false + } + } + return true } func (xyz XYZColor) XYZ() XYZColor { @@ -367,6 +410,35 @@ func Approximate(color Colorer, comparer Comparer) (float64, int) { return bestdist, best + 17 } +// ApproximateAll ищет ближайший цвет среди всех 256 цветов xterm, +// включая 16 системных (их реальный вид зависит от темы терминала) +func ApproximateAll(color Colorer, comparer Comparer) (float64, int) { + bestdist, best := Approximate(color, comparer) + for i, applicant := range SystemLabPalette { + if dist := comparer.Compare(color, applicant); dist < bestdist { + best, bestdist = i+1, dist + } + } + return bestdist, best +} + +// TermRGB возвращает RGB-значение по termbox-атрибуту (1..256). +// Для системных цветов используются стандартные значения xterm. +func TermRGB(c int) RGBColor { + if c-1 < 16 { + return SystemRGBPalette[c-1] + } + return XtermRGBPalette[c-17] +} + +// TermLab возвращает Lab-значение по termbox-атрибуту (1..256) +func TermLab(c int) LabColor { + if c-1 < 16 { + return SystemLabPalette[c-1] + } + return XtermLabPalette[c-17] +} + //color = round(36 * (r * 5) + 6 * (g * 5) + (b * 5) + 16) func HackApproximate(color Colorer) int { c := color.RGB() @@ -623,6 +695,28 @@ var ( */ XtermRGBPalette [240]RGBColor XtermLabPalette [240]LabColor + + // standard xterm defaults for the 16 system colors (0-15); + // actual appearance depends on the terminal theme + SystemRGBPalette = [16]RGBColor{ + {0x00, 0x00, 0x00}, // 0 black + {0xCD, 0x00, 0x00}, // 1 red + {0x00, 0xCD, 0x00}, // 2 green + {0xCD, 0xCD, 0x00}, // 3 yellow + {0x00, 0x00, 0xEE}, // 4 blue + {0xCD, 0x00, 0xCD}, // 5 magenta + {0x00, 0xCD, 0xCD}, // 6 cyan + {0xE5, 0xE5, 0xE5}, // 7 white + {0x7F, 0x7F, 0x7F}, // 8 bright black + {0xFF, 0x00, 0x00}, // 9 bright red + {0x00, 0xFF, 0x00}, // 10 bright green + {0xFF, 0xFF, 0x00}, // 11 bright yellow + {0x5C, 0x5C, 0xFF}, // 12 bright blue + {0xFF, 0x00, 0xFF}, // 13 bright magenta + {0x00, 0xFF, 0xFF}, // 14 bright cyan + {0xFF, 0xFF, 0xFF}, // 15 bright white + } + SystemLabPalette [16]LabColor ) func init() { @@ -646,4 +740,9 @@ func init() { for i, color := range XtermRGBPalette { XtermLabPalette[i] = color.Lab() } + + // calculate Lab palette for the 16 system colors + for i, color := range SystemRGBPalette { + SystemLabPalette[i] = color.Lab() + } } @@ -0,0 +1,7 @@ +module codeberg.org/historia/xterm256-color-palette + +go 1.26.3 + +require github.com/nsf/termbox-go v1.1.1 + +require github.com/mattn/go-runewidth v0.0.9 // indirect @@ -0,0 +1,4 @@ +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY= +github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= |
