Files
gastown/internal/cmd/thanks.go
Ryan Snodgrass e1f2bb8b4b feat(ui): import comprehensive UX system from beads
Import beads' UX design system into gastown:

- Add internal/ui/ package with Ayu theme colors and semantic styling
  - styles.go: AdaptiveColor definitions for light/dark mode
  - terminal.go: TTY detection, NO_COLOR/CLICOLOR support
  - markdown.go: Glamour rendering with agent mode bypass
  - pager.go: Smart paging with GT_PAGER support

- Add colorized help output (internal/cmd/help.go)
  - Group headers in accent color
  - Command names styled for scannability
  - Flag types and defaults muted

- Add gt thanks command (internal/cmd/thanks.go)
  - Contributor display with same logic as bd thanks
  - Styled with Ayu theme colors

- Update gt doctor to match bd doctor UX
  - Category grouping (Core, Infrastructure, Rig, Patrol, etc.)
  - Semantic icons (✓ ⚠ ✖) with Ayu colors
  - Tree connectors for detail lines
  - Summary line with pass/warn/fail counts
  - Warnings section at end with numbered issues

- Migrate existing styles to use ui package
  - internal/style/style.go uses ui.ColorPass etc.
  - internal/tui/feed/styles.go uses ui package colors

Co-Authored-By: SageOx <ox@sageox.ai>
2026-01-09 22:46:06 -08:00

240 lines
6.0 KiB
Go

package cmd
import (
"cmp"
"fmt"
"slices"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/ui"
)
// Style definitions for thanks output using ui package colors
var (
thanksTitleStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ui.ColorWarn)
thanksSubtitleStyle = lipgloss.NewStyle().
Foreground(ui.ColorMuted)
thanksSectionStyle = lipgloss.NewStyle().
Foreground(ui.ColorAccent).
Bold(true)
thanksNameStyle = lipgloss.NewStyle().
Foreground(ui.ColorPass)
thanksDimStyle = lipgloss.NewStyle().
Foreground(ui.ColorMuted)
)
// thanksBoxStyle returns a bordered box style for the thanks header
func thanksBoxStyle(width int) lipgloss.Style {
return lipgloss.NewStyle().
Border(lipgloss.DoubleBorder()).
BorderForeground(ui.ColorMuted).
Padding(1, 4).
Width(width - 4).
Align(lipgloss.Center)
}
// gastownContributors maps HUMAN contributor names to their commit counts.
// Agent names (gastown/*, beads/*, lowercase single-word names) are excluded.
// Generated from: git shortlog -sn --all (then filtered for humans only)
var gastownContributors = map[string]int{
"Steve Yegge": 2056,
"Mike Lady": 19,
"Olivier Debeuf De Rijcker": 13,
"Danno Mayer": 11,
"Dan Shapiro": 7,
"Subhrajit Makur": 7,
"Julian Knutsen": 5,
"Darko Luketic": 4,
"Martin Emde": 4,
"Greg Hughes": 3,
"Avyukth": 2,
"Ben Kraus": 2,
"Joshua Vial": 2,
"Austin Wallace": 1,
"Cameron Palmer": 1,
"Chris Sloane": 1,
"Cong": 1,
"Dave Laird": 1,
"Dave Williams": 1,
"Jacob": 1,
"Johann Taberlet": 1,
"Joshua Samuel": 1,
"Madison Bullard": 1,
"PepijnSenders": 1,
"Raymond Weitekamp": 1,
"Sohail Mohammad": 1,
"Zachary Rosen": 1,
}
var thanksCmd = &cobra.Command{
Use: "thanks",
Short: "Thank the human contributors to Gas Town",
GroupID: GroupDiag,
Long: `Display acknowledgments to all the humans who have contributed
to the Gas Town project. This command celebrates the collaborative
effort behind the multi-agent workspace manager.`,
Run: func(cmd *cobra.Command, args []string) {
printThanksPage()
},
}
// getContributorsSorted returns contributor names sorted by commit count descending
func getContributorsSorted() []string {
names := make([]string, 0, len(gastownContributors))
for name := range gastownContributors {
names = append(names, name)
}
slices.SortFunc(names, func(a, b string) int {
// sort by commit count descending, then by name ascending for ties
countCmp := cmp.Compare(gastownContributors[b], gastownContributors[a])
if countCmp != 0 {
return countCmp
}
return cmp.Compare(a, b)
})
return names
}
// printThanksPage renders the complete thanks page
func printThanksPage() {
fmt.Println()
// get sorted contributors, split into featured (top 20) and rest
sorted := getContributorsSorted()
featuredCount := 20
if len(sorted) < featuredCount {
featuredCount = len(sorted)
}
featured := sorted[:featuredCount]
additional := sorted[featuredCount:]
// calculate content width based on 4 columns
cols := 4
contentWidth := calculateColumnsWidth(featured, cols)
if contentWidth < 60 {
contentWidth = 60
}
// build header content
title := thanksTitleStyle.Render("THANK YOU!")
subtitle := thanksSubtitleStyle.Render("To all the humans who contributed to Gas Town")
headerContent := title + "\n\n" + subtitle
// render header in bordered box
header := thanksBoxStyle(contentWidth).Render(headerContent)
fmt.Println(header)
fmt.Println()
// print featured contributors section
fmt.Println(thanksSectionStyle.Render(" Featured Contributors"))
fmt.Println()
printThanksColumns(featured, cols)
// print additional contributors if any
if len(additional) > 0 {
fmt.Println()
fmt.Println(thanksSectionStyle.Render(" Additional Contributors"))
fmt.Println()
printThanksWrappedList("", additional, contentWidth)
}
fmt.Println()
}
// calculateColumnsWidth determines the width needed for n columns of names
func calculateColumnsWidth(names []string, cols int) int {
maxWidth := 0
for _, name := range names {
if len(name) > maxWidth {
maxWidth = len(name)
}
}
// cap at 20 characters per column
if maxWidth > 20 {
maxWidth = 20
}
// add padding between columns
colWidth := maxWidth + 2
return colWidth * cols
}
// printThanksColumns prints names in n columns, reading left-to-right
func printThanksColumns(names []string, cols int) {
if len(names) == 0 {
return
}
// find max width for alignment
maxWidth := 0
for _, name := range names {
if len(name) > maxWidth {
maxWidth = len(name)
}
}
if maxWidth > 20 {
maxWidth = 20
}
colWidth := maxWidth + 2
// print in rows, reading left to right (matches bd thanks)
for i := 0; i < len(names); i += cols {
fmt.Print(" ")
for j := 0; j < cols && i+j < len(names); j++ {
name := names[i+j]
if len(name) > 20 {
name = name[:17] + "..."
}
// pad BEFORE styling to avoid ANSI code width issues
padded := fmt.Sprintf("%-*s", colWidth, name)
fmt.Print(thanksNameStyle.Render(padded))
}
fmt.Println()
}
}
// printThanksWrappedList prints a comma-separated list with word wrapping
func printThanksWrappedList(label string, names []string, maxWidth int) {
indent := " "
fmt.Print(indent)
lineLen := len(indent)
if label != "" {
fmt.Print(thanksSectionStyle.Render(label) + " ")
lineLen += len(label) + 1
}
for i, name := range names {
suffix := ", "
if i == len(names)-1 {
suffix = ""
}
entry := name + suffix
if lineLen+len(entry) > maxWidth && lineLen > len(indent) {
fmt.Println()
fmt.Print(indent)
lineLen = len(indent)
}
fmt.Print(thanksDimStyle.Render(entry))
lineLen += len(entry)
}
fmt.Println()
}
func init() {
rootCmd.AddCommand(thanksCmd)
}