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>
This commit is contained in:
2026-07-26 17:42:37 +02:00
co-authored by Claude Opus 5
parent 86fb898df4
commit cb1ff98c3d
6 changed files with 194 additions and 67 deletions
+6 -3
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"strconv"
"strings"
"unicode/utf8"
)
// Colors for the prompt, banner and output, using the Catppuccin Mocha palette
@@ -36,10 +37,12 @@ func errorln(msg string) {
fmt.Println(col(cRed, msg))
}
// padRight pads an ASCII string with trailing spaces to width n.
// 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 len(s) < n {
return s + strings.Repeat(" ", n-len(s))
if l := utf8.RuneCountInString(s); l < n {
return s + strings.Repeat(" ", n-l)
}
return s
}