// Package cli provides semantic CLI output with zero external dependencies. package cli import ( "fmt" "strings" "time" ) // Tailwind colour palette (hex strings) const ( ColourBlue50 = "#eff6ff" ColourBlue100 = "#dbeafe" ColourBlue200 = "#bfdbfe" ColourBlue300 = "#93c5fd" ColourBlue400 = "#60a5fa" ColourBlue500 = "#3b82f6" ColourBlue600 = "#2563eb" ColourBlue700 = "#1d4ed8" ColourGreen400 = "#4ade80" ColourGreen500 = "#22c55e" ColourGreen600 = "#16a34a" ColourRed400 = "#f87171" ColourRed500 = "#ef4444" ColourRed600 = "#dc2626" ColourAmber400 = "#fbbf24" ColourAmber500 = "#f59e0b" ColourAmber600 = "#d97706" ColourOrange500 = "#f97316" ColourYellow500 = "#eab308" ColourEmerald500 = "#10b981" ColourPurple500 = "#a855f7" ColourViolet400 = "#a78bfa" ColourViolet500 = "#8b5cf6" ColourIndigo500 = "#6366f1" ColourCyan500 = "#06b6d4" ColourGray50 = "#f9fafb" ColourGray100 = "#f3f4f6" ColourGray200 = "#e5e7eb" ColourGray300 = "#d1d5db" ColourGray400 = "#9ca3af" ColourGray500 = "#6b7280" ColourGray600 = "#4b5563" ColourGray700 = "#374151" ColourGray800 = "#1f2937" ColourGray900 = "#111827" ) // Core styles var ( SuccessStyle = NewStyle().Bold().Foreground(ColourGreen500) ErrorStyle = NewStyle().Bold().Foreground(ColourRed500) WarningStyle = NewStyle().Bold().Foreground(ColourAmber500) InfoStyle = NewStyle().Foreground(ColourBlue400) DimStyle = NewStyle().Dim().Foreground(ColourGray500) MutedStyle = NewStyle().Foreground(ColourGray600) BoldStyle = NewStyle().Bold() KeyStyle = NewStyle().Foreground(ColourGray400) ValueStyle = NewStyle().Foreground(ColourGray200) AccentStyle = NewStyle().Foreground(ColourCyan500) LinkStyle = NewStyle().Foreground(ColourBlue500).Underline() HeaderStyle = NewStyle().Bold().Foreground(ColourGray200) TitleStyle = NewStyle().Bold().Foreground(ColourBlue500) CodeStyle = NewStyle().Foreground(ColourGray300) NumberStyle = NewStyle().Foreground(ColourBlue300) RepoStyle = NewStyle().Bold().Foreground(ColourBlue500) ) // Truncate shortens a string to max length with ellipsis. func Truncate(s string, max int) string { if len(s) <= max { return s } if max <= 3 { return s[:max] } return s[:max-3] + "..." } // Pad right-pads a string to width. func Pad(s string, width int) string { if len(s) >= width { return s } return s + strings.Repeat(" ", width-len(s)) } // FormatAge formats a time as human-readable age (e.g., "2h ago", "3d ago"). func FormatAge(t time.Time) string { d := time.Since(t) switch { case d < time.Minute: return "just now" case d < time.Hour: return fmt.Sprintf("%dm ago", int(d.Minutes())) case d < 24*time.Hour: return fmt.Sprintf("%dh ago", int(d.Hours())) case d < 7*24*time.Hour: return fmt.Sprintf("%dd ago", int(d.Hours()/24)) case d < 30*24*time.Hour: return fmt.Sprintf("%dw ago", int(d.Hours()/(24*7))) default: return fmt.Sprintf("%dmo ago", int(d.Hours()/(24*30))) } } // Table renders tabular data with aligned columns. // HLCRF is for layout; Table is for tabular data - they serve different purposes. type Table struct { Headers []string Rows [][]string Style TableStyle } // TableStyle configures the appearance of table output. type TableStyle struct { HeaderStyle *AnsiStyle CellStyle *AnsiStyle Separator string } // DefaultTableStyle returns sensible defaults. func DefaultTableStyle() TableStyle { return TableStyle{ HeaderStyle: HeaderStyle, CellStyle: nil, Separator: " ", } } // NewTable creates a table with headers. func NewTable(headers ...string) *Table { return &Table{ Headers: headers, Style: DefaultTableStyle(), } } // AddRow adds a row to the table. func (t *Table) AddRow(cells ...string) *Table { t.Rows = append(t.Rows, cells) return t } // String renders the table. func (t *Table) String() string { if len(t.Headers) == 0 && len(t.Rows) == 0 { return "" } // Calculate column widths cols := len(t.Headers) if cols == 0 && len(t.Rows) > 0 { cols = len(t.Rows[0]) } widths := make([]int, cols) for i, h := range t.Headers { if len(h) > widths[i] { widths[i] = len(h) } } for _, row := range t.Rows { for i, cell := range row { if i < cols && len(cell) > widths[i] { widths[i] = len(cell) } } } var sb strings.Builder sep := t.Style.Separator // Headers if len(t.Headers) > 0 { for i, h := range t.Headers { if i > 0 { sb.WriteString(sep) } styled := Pad(h, widths[i]) if t.Style.HeaderStyle != nil { styled = t.Style.HeaderStyle.Render(styled) } sb.WriteString(styled) } sb.WriteString("\n") } // Rows for _, row := range t.Rows { for i, cell := range row { if i > 0 { sb.WriteString(sep) } styled := Pad(cell, widths[i]) if t.Style.CellStyle != nil { styled = t.Style.CellStyle.Render(styled) } sb.WriteString(styled) } sb.WriteString("\n") } return sb.String() } // Render prints the table to stdout. func (t *Table) Render() { fmt.Print(t.String()) }