// Package style provides consistent terminal styling using Lipgloss. package style import ( "fmt" "strings" "github.com/charmbracelet/lipgloss" ) // Column defines a table column with name and width. type Column struct { Name string Width int Align Alignment Style lipgloss.Style } // Alignment specifies column text alignment. type Alignment int const ( AlignLeft Alignment = iota AlignRight AlignCenter ) // Table provides styled table rendering. type Table struct { columns []Column rows [][]string headerSep bool indent string headerStyle lipgloss.Style } // NewTable creates a new table with the given columns. func NewTable(columns ...Column) *Table { return &Table{ columns: columns, headerSep: true, indent: " ", headerStyle: Bold, } } // SetIndent sets the left indent for the table. func (t *Table) SetIndent(indent string) *Table { t.indent = indent return t } // SetHeaderSeparator enables/disables the header separator line. func (t *Table) SetHeaderSeparator(enabled bool) *Table { t.headerSep = enabled return t } // AddRow adds a row of values to the table. func (t *Table) AddRow(values ...string) *Table { // Pad with empty strings if needed for len(values) < len(t.columns) { values = append(values, "") } t.rows = append(t.rows, values) return t } // Render returns the formatted table string. func (t *Table) Render() string { if len(t.columns) == 0 { return "" } var sb strings.Builder // Render header sb.WriteString(t.indent) for i, col := range t.columns { text := t.headerStyle.Render(col.Name) sb.WriteString(t.pad(text, col.Name, col.Width, col.Align)) if i < len(t.columns)-1 { sb.WriteString(" ") } } sb.WriteString("\n") // Render separator if t.headerSep { sb.WriteString(t.indent) totalWidth := 0 for i, col := range t.columns { totalWidth += col.Width if i < len(t.columns)-1 { totalWidth++ // space between columns } } sb.WriteString(Dim.Render(strings.Repeat("─", totalWidth))) sb.WriteString("\n") } // Render rows for _, row := range t.rows { sb.WriteString(t.indent) for i, col := range t.columns { val := "" if i < len(row) { val = row[i] } // Truncate if too long plainVal := stripAnsi(val) if len(plainVal) > col.Width { val = plainVal[:col.Width-3] + "..." } // Apply column style if set if col.Style.Value() != "" { val = col.Style.Render(val) } sb.WriteString(t.pad(val, plainVal, col.Width, col.Align)) if i < len(t.columns)-1 { sb.WriteString(" ") } } sb.WriteString("\n") } return sb.String() } // pad pads text to width, accounting for ANSI escape sequences. // styledText is the text with ANSI codes, plainText is without. func (t *Table) pad(styledText, plainText string, width int, align Alignment) string { plainLen := len(plainText) if plainLen >= width { return styledText } padding := width - plainLen switch align { case AlignRight: return strings.Repeat(" ", padding) + styledText case AlignCenter: left := padding / 2 right := padding - left return strings.Repeat(" ", left) + styledText + strings.Repeat(" ", right) default: // AlignLeft return styledText + strings.Repeat(" ", padding) } } // stripAnsi removes ANSI escape sequences from a string. func stripAnsi(s string) string { var result strings.Builder inEscape := false for i := 0; i < len(s); i++ { if s[i] == '\x1b' { inEscape = true continue } if inEscape { if s[i] == 'm' { inEscape = false } continue } result.WriteByte(s[i]) } return result.String() } // PhaseTable renders the molecule phase transition table. func PhaseTable() string { return ` Phase Flow: discovery ──┬──→ structural ──→ tactical ──→ synthesis │ (sequential) (parallel) (single) └─── (parallel) ┌─────────────┬─────────────┬─────────────┬─────────────────────┐ │ Phase │ Parallelism │ Blocks │ Purpose │ ├─────────────┼─────────────┼─────────────┼─────────────────────┤ │ discovery │ full │ (nothing) │ Inventory, gather │ │ structural │ sequential │ discovery │ Big-picture review │ │ tactical │ parallel │ structural │ Detailed work │ │ synthesis │ single │ tactical │ Aggregate results │ └─────────────┴─────────────┴─────────────┴─────────────────────┘` } // MoleculeLifecycleASCII renders the molecule lifecycle diagram. func MoleculeLifecycleASCII() string { return ` Proto (template) │ ▼ bond ┌─────────────────┐ │ Mol (durable) │ │ Wisp (ephemeral)│ └────────┬────────┘ │ ┌──────┴──────┐ ▼ ▼ burn squash (no record) (→ digest)` } // DAGProgress renders a DAG progress visualization. // steps is a map of step name to status (done, in_progress, ready, blocked). func DAGProgress(steps map[string]string, phases []string) string { var sb strings.Builder icons := map[string]string{ "done": "✓", "in_progress": "⧖", "ready": "○", "blocked": "◌", } colors := map[string]lipgloss.Style{ "done": Success, "in_progress": Warning, "ready": Info, "blocked": Dim, } for _, phase := range phases { sb.WriteString(fmt.Sprintf(" %s\n", Bold.Render(phase))) for name, status := range steps { if strings.HasPrefix(name, phase+"-") || strings.HasPrefix(name, phase+"/") { icon := icons[status] style := colors[status] stepName := strings.TrimPrefix(strings.TrimPrefix(name, phase+"-"), phase+"/") sb.WriteString(fmt.Sprintf(" %s %s\n", style.Render(icon), stepName)) } } } return sb.String() } // SuggestionBox renders a "did you mean" suggestion box. func SuggestionBox(message string, suggestions []string, hint string) string { var sb strings.Builder sb.WriteString(fmt.Sprintf("\n%s %s\n", ErrorPrefix, message)) if len(suggestions) > 0 { sb.WriteString("\n Did you mean?\n") for _, s := range suggestions { sb.WriteString(fmt.Sprintf(" • %s\n", s)) } } if hint != "" { sb.WriteString(fmt.Sprintf("\n %s\n", Dim.Render(hint))) } return sb.String() } // ProgressBar renders a simple progress bar. func ProgressBar(percent int, width int) string { if percent < 0 { percent = 0 } if percent > 100 { percent = 100 } filled := (percent * width) / 100 empty := width - filled bar := strings.Repeat("█", filled) + strings.Repeat("░", empty) return fmt.Sprintf("[%s] %d%%", bar, percent) }