Files
mgsh/colors.go
T
mikeandClaude Opus 5 cb1ff98c3d Lay overview out as a table
The status field was not a column: "*", "↑2", "✓" and "✓ (no upstream)"
are four different widths, so everything after them started somewhere
else on every line and the eye had to hunt along each row instead of
going down one.

Each field now has its own measured column: name (with the branch
appended when it is not master/main), a one-character dirty marker, the
sync state, host and age, then the mirrors. "(no upstream)" was fifteen
columns wide for something that is not even a problem, and is now "–".
Colour weights the row rather than decorating it -- a project that is
clean and in sync goes grey, the arrows and the dirty marker keep their
colour -- and the rows needing action sort to the top, alphabetically
within each group so positions stay predictable.

padRight counted bytes, which was fine while everything it padded was
ASCII; the arrows and check marks are three bytes and one column, so it
counts runes now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 17:42:37 +02:00

94 lines
2.7 KiB
Go

package main
import (
"fmt"
"strconv"
"strings"
"unicode/utf8"
)
// Colors for the prompt, banner and output, using the Catppuccin Mocha palette
// as 24-bit truecolor escapes
// (https://terminalcolors.com/themes/catppuccin/mocha/).
const (
cReset = "\033[0m"
cBold = "\033[1m"
cDim = "\033[2m"
cGreen = "\033[38;2;166;227;161m" // Green #a6e3a1
cYellow = "\033[38;2;249;226;175m" // Yellow #f9e2af
cRed = "\033[38;2;243;139;168m" // Red #f38ba8
cCyan = "\033[38;2;148;226;213m" // Teal #94e2d5
cPurple = "\033[38;2;203;166;247m" // Mauve #cba6f7
cWhite = "\033[38;2;205;214;244m" // Text #cdd6f4
cGray = "\033[38;2;108;112;134m" // Overlay0 #6c7086
)
// col wraps s in color c, but only when color output is enabled.
func col(c, s string) string {
if useColor {
return c + s + cReset
}
return s
}
// errorln prints an error/status message in red (when color is enabled).
func errorln(msg string) {
fmt.Println(col(cRed, msg))
}
// padRight pads s with trailing spaces to a width of n columns. It counts
// runes, not bytes: the overview pads fields holding ↑ ↓ ✓, each of which is
// one column wide but three bytes long.
func padRight(s string, n int) string {
if l := utf8.RuneCountInString(s); l < n {
return s + strings.Repeat(" ", n-l)
}
return s
}
// formatRepoList renders the server listing for `list`: the name first, in a
// column wide enough for the longest one, then the date, and for archives the
// size. Names come first because that is what the eye scans for; putting the
// ragged date there instead is what made the old output hard to read.
//
// The order is left as it arrives: `ls -ltr` sorts by modification time, and
// `push` touches the bare repository, so the most recently worked-on project
// ends up closest to the prompt.
func formatRepoList(entries []lsEntry, withSize bool) string {
width := 0
for _, e := range entries {
if len(e.name) > width {
width = len(e.name)
}
}
var b strings.Builder
for _, e := range entries {
fmt.Fprintf(&b, " %s %s", col(cGreen, padRight(e.name, width)), col(cYellow, e.date))
if withSize {
fmt.Fprintf(&b, " %s", col(cGray, fmt.Sprintf("%7s", humanSize(e.size))))
}
b.WriteByte('\n')
}
return b.String()
}
// humanSize renders a byte count compactly, the way `ls -h` does: a decimal
// only while it still carries information, so "3.2M" but "512K".
func humanSize(n int64) string {
const unit = 1024
if n < unit {
return strconv.FormatInt(n, 10) + "B"
}
div, exp := int64(unit), 0
for v := n / unit; v >= unit && exp < 4; v /= unit {
div *= unit
exp++
}
v := float64(n) / float64(div)
if v < 10 {
return fmt.Sprintf("%.1f%c", v, "KMGTP"[exp])
}
return fmt.Sprintf("%.0f%c", v, "KMGTP"[exp])
}