Files
mgsh/colors.go
T
mikeandClaude Opus 5 8c68b28dc2 Format list as an aligned table
The name is what the eye looks for, but it came last, behind a ragged
date column, so nothing lined up. Worse, colorRepoLine rebuilt the line
with strings.Fields and single spaces, which destroyed the alignment ls
had produced -- the output was aligned only when colour was off.

The listing line is now parsed properly instead of being split at the
size field: name, date and size come out as fields, the date is re-padded
to a fixed twelve columns so "Sep 28  2016" and "Jan  3 14:32" agree, and
the name leads in a column sized to the longest entry. Colour decorates
that layout without changing it, which a test now checks by stripping the
escapes and comparing. `list -a` shows archive sizes, which were parsed
and thrown away before.

An empty result says so instead of printing nothing, which was
indistinguishable from a failure, and the count line matches the rest of
mgsh. The pattern now filters on the repository name rather than the
whole listing line: matching the owner or the date was never intended and
`list 2016` quietly did it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:53:54 +02:00

91 lines
2.5 KiB
Go

package main
import (
"fmt"
"strconv"
"strings"
)
// 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 an ASCII string with trailing spaces to width n.
func padRight(s string, n int) string {
if len(s) < n {
return s + strings.Repeat(" ", n-len(s))
}
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])
}