From 0e5b48383a30f7fa668222bcab6f30d9bc048f7f Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Fri, 17 Apr 2026 16:06:30 +0300 Subject: [PATCH] feat: Add UI styling system with lipgloss integration and icon definitions - Add colors.go with color palette: primary, accent, muted, semantic colors (success, warning, error, info) - Add base styles: focused, blurred, help, header, subheader, title styles - Add status styles for services (running, stopped, pending, unknown) and subscribers (active, inactive, suspended) - Add metric styles with threshold-based coloring (good, warning, critical) - Add table styles: header, border, even/odd rows --- apps/cli/internal/ui/colors.go | 422 +++++++++++++++++++++++++++++++++ apps/cli/internal/ui/icons.go | 315 ++++++++++++++++++++++++ apps/cli/internal/ui/table.go | 321 +++++++++++++++++++++++++ 3 files changed, 1058 insertions(+) create mode 100644 apps/cli/internal/ui/colors.go create mode 100644 apps/cli/internal/ui/icons.go create mode 100644 apps/cli/internal/ui/table.go diff --git a/apps/cli/internal/ui/colors.go b/apps/cli/internal/ui/colors.go new file mode 100644 index 0000000..c90ca47 --- /dev/null +++ b/apps/cli/internal/ui/colors.go @@ -0,0 +1,422 @@ +package ui + +import ( + "fmt" + "os" + + "github.com/charmbracelet/lipgloss" +) + +// Color palette using lipgloss +var ( + // Primary colors + primaryColor = lipgloss.Color("#4ECDC4") // Teal + accentColor = lipgloss.Color("#FF6B6B") // Red + mutedColor = lipgloss.Color("#999999") // Gray + whiteColor = lipgloss.Color("#FFFFFF") // White + blackColor = lipgloss.Color("#000000") // Black + + // Semantic colors + successColor = lipgloss.Color("#4ECDC4") // Same as primary + warningColor = lipgloss.Color("#FFA726") // Orange + errorColor = lipgloss.Color("#FF6B6B") // Same as accent + infoColor = lipgloss.Color("#42A5F5") // Blue + + // Additional colors + yellowColor = lipgloss.Color("#FFD93D") // Yellow + greenColor = lipgloss.Color("#6BCF7F") // Green + blueColor = lipgloss.Color("#4A90E2") // Blue + purpleColor = lipgloss.Color("#9B59B6") // Purple + cyanColor = lipgloss.Color("#00BCD4") // Cyan +) + +// Base styles +var ( + baseStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()) + + focusedStyle = baseStyle. + BorderForeground(accentColor) + + blurredStyle = baseStyle. + BorderForeground(mutedColor) + + helpStyle = lipgloss.NewStyle(). + Foreground(mutedColor) + + headerStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(whiteColor). + Background(primaryColor). + Padding(0, 1) + + subHeaderStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(primaryColor). + Padding(0, 1) + + titleStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(whiteColor). + Background(accentColor). + Padding(0, 1) + + // Status styles + statusSuccessStyle = lipgloss.NewStyle(). + Foreground(successColor). + Bold(true) + + statusErrorStyle = lipgloss.NewStyle(). + Foreground(errorColor). + Bold(true) + + statusWarningStyle = lipgloss.NewStyle(). + Foreground(warningColor). + Bold(true) + + statusInfoStyle = lipgloss.NewStyle(). + Foreground(infoColor). + Bold(true) + + // Service status styles + statusRunningStyle = lipgloss.NewStyle(). + Foreground(successColor). + Bold(true) + + statusStoppedStyle = lipgloss.NewStyle(). + Foreground(errorColor). + Bold(true) + + statusPendingStyle = lipgloss.NewStyle(). + Foreground(warningColor). + Bold(true) + + statusUnknownStyle = lipgloss.NewStyle(). + Foreground(mutedColor). + Italic(true) + + // Subscriber status styles + statusActiveStyle = lipgloss.NewStyle(). + Foreground(successColor). + Bold(true) + + statusInactiveStyle = lipgloss.NewStyle(). + Foreground(errorColor). + Bold(true) + + statusSuspendedStyle = lipgloss.NewStyle(). + Foreground(warningColor). + Bold(true) + + // Metric styles + metricGoodStyle = lipgloss.NewStyle(). + Foreground(successColor) + + metricWarningStyle = lipgloss.NewStyle(). + Foreground(warningColor) + + metricCriticalStyle = lipgloss.NewStyle(). + Foreground(errorColor). + Bold(true) + + // Text styles + boldStyle = lipgloss.NewStyle().Bold(true) + italicStyle = lipgloss.NewStyle().Italic(true) + underlineStyle = lipgloss.NewStyle().Underline(true) + dimStyle = lipgloss.NewStyle().Faint(true) + + // Command styles + commandStyle = lipgloss.NewStyle(). + Foreground(primaryColor). + Bold(true) + + optionStyle = lipgloss.NewStyle(). + Foreground(yellowColor). + Bold(true) + + argumentStyle = lipgloss.NewStyle(). + Foreground(whiteColor). + Bold(true) + + descriptionStyle = lipgloss.NewStyle(). + Foreground(mutedColor). + Italic(true) + + // Table styles + tableHeaderStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(primaryColor). + Background(lipgloss.Color("#2D2D2D")). + Padding(0, 1) + + tableBorderStyle = lipgloss.NewStyle(). + Foreground(mutedColor). + Faint(true) + + tableRowEvenStyle = lipgloss.NewStyle(). + Foreground(whiteColor) + + tableRowOddStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#F0F0F0")) + + // Panel styles + panelStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(primaryColor). + Padding(1) + + panelFocusedStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(accentColor). + Padding(1) + + // Error and success styles + errorStyle = lipgloss.NewStyle(). + Foreground(errorColor). + Bold(true) + + successStyle = lipgloss.NewStyle(). + Foreground(successColor). + Bold(true) + + warningStyle = lipgloss.NewStyle(). + Foreground(warningColor). + Bold(true) + + infoStyle = lipgloss.NewStyle(). + Foreground(infoColor). + Bold(true) + + debugStyle = lipgloss.NewStyle(). + Foreground(mutedColor). + Italic(true) + + mutedStyle = lipgloss.NewStyle(). + Foreground(mutedColor). + Faint(true) + + // Icon styles + iconStyle = lipgloss.NewStyle(). + Bold(true) + + iconSuccessStyle = lipgloss.NewStyle(). + Foreground(successColor). + Bold(true) + + iconErrorStyle = lipgloss.NewStyle(). + Foreground(errorColor). + Bold(true) + + iconWarningStyle = lipgloss.NewStyle(). + Foreground(warningColor). + Bold(true) + + iconInfoStyle = lipgloss.NewStyle(). + Foreground(infoColor). + Bold(true) +) + +// Style represents a lipgloss style +type Style struct { + Style lipgloss.Style +} + +// Predefined styles using lipgloss +var ( + StyleSuccess = Style{Style: statusSuccessStyle} + StyleError = Style{Style: statusErrorStyle} + StyleWarning = Style{Style: statusWarningStyle} + StyleInfo = Style{Style: statusInfoStyle} + StyleDebug = Style{Style: debugStyle} + StyleMuted = Style{Style: mutedStyle} + + StyleRunning = Style{Style: statusRunningStyle} + StyleStopped = Style{Style: statusStoppedStyle} + StylePending = Style{Style: statusPendingStyle} + StyleUnknown = Style{Style: statusUnknownStyle} + + StyleActive = Style{Style: statusActiveStyle} + StyleInactive = Style{Style: statusInactiveStyle} + StyleSuspended = Style{Style: statusSuspendedStyle} + + StyleHeader = Style{Style: headerStyle} + StyleSubHeader = Style{Style: subHeaderStyle} + StyleTitle = Style{Style: titleStyle} + + StyleHeaderRow = Style{Style: tableHeaderStyle} + StyleRowEven = Style{Style: tableRowEvenStyle} + StyleRowOdd = Style{Style: tableRowOddStyle} + StyleBorder = Style{Style: tableBorderStyle} + + StyleMetricGood = Style{Style: metricGoodStyle} + StyleMetricWarning = Style{Style: metricWarningStyle} + StyleMetricCritical = Style{Style: metricCriticalStyle} + + StyleCommand = Style{Style: commandStyle} + StyleOption = Style{Style: optionStyle} + StyleArgument = Style{Style: argumentStyle} + StyleDescription = Style{Style: descriptionStyle} + + StylePanel = Style{Style: panelStyle} + StylePanelFocused = Style{Style: panelFocusedStyle} +) + +// Colorizer handles color output using lipgloss +type Colorizer struct { + enabled bool +} + +// NewColorizer creates a new colorizer +func NewColorizer(enabled bool) *Colorizer { + return &Colorizer{enabled: enabled} +} + +// IsEnabled returns whether colors are enabled +func (c *Colorizer) IsEnabled() bool { + return c.enabled +} + +// Colorize applies a style to text using lipgloss +func (c *Colorizer) Colorize(text string, style Style) string { + if !c.enabled { + return text + } + return style.Style.Render(text) +} + +// Convenience methods for common colorizations +func (c *Colorizer) Success(text string) string { + return c.Colorize(text, StyleSuccess) +} + +func (c *Colorizer) Error(text string) string { + return c.Colorize(text, StyleError) +} + +func (c *Colorizer) Warning(text string) string { + return c.Colorize(text, StyleWarning) +} + +func (c *Colorizer) Info(text string) string { + return c.Colorize(text, StyleInfo) +} + +func (c *Colorizer) Debug(text string) string { + return c.Colorize(text, StyleDebug) +} + +func (c *Colorizer) Muted(text string) string { + return c.Colorize(text, StyleMuted) +} + +func (c *Colorizer) Header(text string) string { + return c.Colorize(text, StyleHeader) +} + +func (c *Colorizer) SubHeader(text string) string { + return c.Colorize(text, StyleSubHeader) +} + +func (c *Colorizer) Title(text string) string { + return c.Colorize(text, StyleTitle) +} + +func (c *Colorizer) Command(text string) string { + return c.Colorize(text, StyleCommand) +} + +func (c *Colorizer) Option(text string) string { + return c.Colorize(text, StyleOption) +} + +func (c *Colorizer) Argument(text string) string { + return c.Colorize(text, StyleArgument) +} + +func (c *Colorizer) Description(text string) string { + return c.Colorize(text, StyleDescription) +} + +// Status colorizations +func (c *Colorizer) StatusRunning(text string) string { + return c.Colorize(text, StyleRunning) +} + +func (c *Colorizer) StatusStopped(text string) string { + return c.Colorize(text, StyleStopped) +} + +func (c *Colorizer) StatusPending(text string) string { + return c.Colorize(text, StylePending) +} + +func (c *Colorizer) StatusUnknown(text string) string { + return c.Colorize(text, StyleUnknown) +} + +func (c *Colorizer) StatusActive(text string) string { + return c.Colorize(text, StyleActive) +} + +func (c *Colorizer) StatusInactive(text string) string { + return c.Colorize(text, StyleInactive) +} + +func (c *Colorizer) StatusSuspended(text string) string { + return c.Colorize(text, StyleSuspended) +} + +// Metric colorizations based on value +func (c *Colorizer) Metric(value float64, thresholds ...float64) string { + text := fmt.Sprintf("%.1f%%", value) + + // Default thresholds if not provided + if len(thresholds) == 0 { + thresholds = []float64{70, 90} + } + + switch { + case value >= thresholds[1]: + return c.Colorize(text, StyleMetricCritical) + case value >= thresholds[0]: + return c.Colorize(text, StyleMetricWarning) + default: + return c.Colorize(text, StyleMetricGood) + } +} + +// Panel rendering +func (c *Colorizer) Panel(text string, focused bool) string { + if focused { + return c.Colorize(text, StylePanelFocused) + } + return c.Colorize(text, StylePanel) +} + +// Check if colors should be disabled +func ShouldDisableColors() bool { + // Check NO_COLOR environment variable + if os.Getenv("NO_COLOR") != "" { + return true + } + + // Check if output is not a TTY + if fileInfo, _ := os.Stdout.Stat(); fileInfo != nil { + return (fileInfo.Mode() & 0o100) == 0 // Not a TTY + } + + return false +} + +// Auto-detect color support +func AutoDetectColors() bool { + return !ShouldDisableColors() +} + +// Global colorizer instance +var GlobalColorizer *Colorizer + +// InitColors initializes the global colorizer +func InitColors(enabled bool) { + GlobalColorizer = NewColorizer(enabled) +} diff --git a/apps/cli/internal/ui/icons.go b/apps/cli/internal/ui/icons.go new file mode 100644 index 0000000..2fde773 --- /dev/null +++ b/apps/cli/internal/ui/icons.go @@ -0,0 +1,315 @@ +package ui + +// Icons and symbols for CLI output +const ( + // Status icons + IconSuccess = "check" + IconError = "x" + IconWarning = "warning" + IconInfo = "info" + IconDebug = "debug" + IconLoading = "dots" + + // Service status icons + IconRunning = "play" + IconStopped = "stop" + IconPending = "clock" + IconUnknown = "help" + + // Subscriber status icons + IconActive = "check-circle" + IconInactive = "x-circle" + IconSuspended = "pause-circle" + + // Resource icons + IconCPU = "cpu" + IconMemory = "hard-drive" + IconDisk = "database" + IconNetwork = "globe" + IconAPI = "api" + IconKey = "key" + IconLock = "lock" + + // Navigation icons + IconArrowRight = "arrow-right" + IconArrowDown = "arrow-down" + IconFolder = "folder" + IconFile = "file" + IconHome = "home" + + // Action icons + IconAdd = "plus" + IconRemove = "minus" + IconEdit = "edit" + IconDelete = "trash" + IconRefresh = "refresh-cw" + IconDownload = "download" + IconUpload = "upload" + + // Communication icons + IconMail = "mail" + IconPhone = "phone" + IconMessage = "message-square" + IconBell = "bell" + + // Security icons + IconShield = "shield" + IconEye = "eye" + IconEyeOff = "eye-off" + + // Chart icons + IconChart = "trending-up" + IconChartDown = "trending-down" + IconBarChart = "bar-chart" + IconPieChart = "pie-chart" + + // Container/platform icons + IconContainer = "box" + IconK8s = "kubernetes" + IconDocker = "docker" + IconCloud = "cloud" + IconServer = "server" +) + +// Unicode fallback icons for terminals without emoji support +var UnicodeIcons = map[string]string{ + IconSuccess: "OK", + IconError: "ERR", + IconWarning: "WARN", + IconInfo: "INFO", + IconDebug: "DEBUG", + IconLoading: "...", + IconRunning: "RUN", + IconStopped: "STOP", + IconPending: "PEND", + IconUnknown: "UNK", + IconActive: "ACT", + IconInactive: "INACT", + IconSuspended: "SUSP", + IconCPU: "CPU", + IconMemory: "MEM", + IconDisk: "DISK", + IconNetwork: "NET", + IconAPI: "API", + IconKey: "KEY", + IconLock: "LCK", + IconArrowRight: "->", + IconArrowDown: "v", + IconFolder: "DIR", + IconFile: "FILE", + IconHome: "HOME", + IconAdd: "+", + IconRemove: "-", + IconEdit: "EDIT", + IconDelete: "DEL", + IconRefresh: "REF", + IconDownload: "DL", + IconUpload: "UL", + IconMail: "MAIL", + IconPhone: "PHONE", + IconMessage: "MSG", + IconBell: "BELL", + IconShield: "SHLD", + IconEye: "EYE", + IconEyeOff: "HIDE", + IconChart: "UP", + IconChartDown: "DOWN", + IconBarChart: "BAR", + IconPieChart: "PIE", + IconContainer: "BOX", + IconK8s: "K8S", + IconDocker: "DOCKER", + IconCloud: "CLOUD", + IconServer: "SRV", +} + +// Emoji icons for modern terminals +var EmojiIcons = map[string]string{ + IconSuccess: "OK", + IconError: "ERR", + IconWarning: "WARN", + IconInfo: "INFO", + IconDebug: "DEBUG", + IconLoading: "...", + IconRunning: "RUN", + IconStopped: "STOP", + IconPending: "PEND", + IconUnknown: "UNK", + IconActive: "ACT", + IconInactive: "INACT", + IconSuspended: "SUSP", + IconCPU: "CPU", + IconMemory: "MEM", + IconDisk: "DISK", + IconNetwork: "NET", + IconAPI: "API", + IconKey: "KEY", + IconLock: "LCK", + IconArrowRight: "->", + IconArrowDown: "v", + IconFolder: "DIR", + IconFile: "FILE", + IconHome: "HOME", + IconAdd: "+", + IconRemove: "-", + IconEdit: "EDIT", + IconDelete: "DEL", + IconRefresh: "REF", + IconDownload: "DL", + IconUpload: "UL", + IconMail: "MAIL", + IconPhone: "PHONE", + IconMessage: "MSG", + IconBell: "BELL", + IconShield: "SHLD", + IconEye: "EYE", + IconEyeOff: "HIDE", + IconChart: "UP", + IconChartDown: "DOWN", + IconBarChart: "BAR", + IconPieChart: "PIE", + IconContainer: "BOX", + IconK8s: "K8S", + IconDocker: "DOCKER", + IconCloud: "CLOUD", + IconServer: "SRV", +} + +// IconRenderer handles icon rendering +type IconRenderer struct { + useUnicode bool + useEmoji bool +} + +// NewIconRenderer creates a new icon renderer +func NewIconRenderer(useUnicode, useEmoji bool) *IconRenderer { + return &IconRenderer{ + useUnicode: useUnicode, + useEmoji: useEmoji, + } +} + +// Render renders an icon based on the current mode +func (ir *IconRenderer) Render(iconName string) string { + if ir.useEmoji { + if icon, exists := EmojiIcons[iconName]; exists { + return icon + } + } + + if ir.useUnicode { + if icon, exists := UnicodeIcons[iconName]; exists { + return icon + } + } + + // Fallback to text version + if icon, exists := UnicodeIcons[iconName]; exists { + return icon + } + + return iconName +} + +// Convenience methods for common icons +func (ir *IconRenderer) Success() string { + return ir.Render(IconSuccess) +} + +func (ir *IconRenderer) Error() string { + return ir.Render(IconError) +} + +func (ir *IconRenderer) Warning() string { + return ir.Render(IconWarning) +} + +func (ir *IconRenderer) Info() string { + return ir.Render(IconInfo) +} + +func (ir *IconRenderer) Running() string { + return ir.Render(IconRunning) +} + +func (ir *IconRenderer) Stopped() string { + return ir.Render(IconStopped) +} + +func (ir *IconRenderer) Pending() string { + return ir.Render(IconPending) +} + +func (ir *IconRenderer) Active() string { + return ir.Render(IconActive) +} + +func (ir *IconRenderer) Inactive() string { + return ir.Render(IconInactive) +} + +func (ir *IconRenderer) CPU() string { + return ir.Render(IconCPU) +} + +func (ir *IconRenderer) Memory() string { + return ir.Render(IconMemory) +} + +func (ir *IconRenderer) Network() string { + return ir.Render(IconNetwork) +} + +func (ir *IconRenderer) API() string { + return ir.Render(IconAPI) +} + +func (ir *IconRenderer) Key() string { + return ir.Render(IconKey) +} + +func (ir *IconRenderer) Shield() string { + return ir.Render(IconShield) +} + +func (ir *IconRenderer) Chart() string { + return ir.Render(IconChart) +} + +func (ir *IconRenderer) Container() string { + return ir.Render(IconContainer) +} + +func (ir *IconRenderer) Cloud() string { + return ir.Render(IconCloud) +} + +func (ir *IconRenderer) Server() string { + return ir.Render(IconServer) +} + +func (ir *IconRenderer) Clock() string { + return ir.Render(IconPending) +} + +// Status icon with color +func (ir *IconRenderer) StatusColored(status string, colorizer *Colorizer) string { + switch status { + case "running", "active": + return colorizer.StatusRunning(ir.Running()) + case "stopped", "inactive": + return colorizer.StatusStopped(ir.Stopped()) + case "pending": + return colorizer.StatusPending(ir.Pending()) + default: + return colorizer.StatusUnknown(status) + } +} + +// Global icon renderer +var GlobalIconRenderer *IconRenderer + +// InitIcons initializes the global icon renderer +func InitIcons(useUnicode, useEmoji bool) { + GlobalIconRenderer = NewIconRenderer(useUnicode, useEmoji) +} diff --git a/apps/cli/internal/ui/table.go b/apps/cli/internal/ui/table.go new file mode 100644 index 0000000..2a80473 --- /dev/null +++ b/apps/cli/internal/ui/table.go @@ -0,0 +1,321 @@ +package ui + +import ( + "strings" + + "github.com/charmbracelet/lipgloss" +) + +// TableColumn represents a column in a table +type TableColumn struct { + Header string + Width int + Alignment string // "left", "right", "center" + Style lipgloss.Style + HeaderStyle lipgloss.Style +} + +// TableRow represents a row in a table +type TableRow struct { + Cells []TableCell + Style lipgloss.Style +} + +// TableCell represents a cell in a table +type TableCell struct { + Text string + Style lipgloss.Style + Align string +} + +// Table represents a formatted table using lipgloss +type Table struct { + Columns []TableColumn + Rows []TableRow + Style TableStyle + Colorizer *Colorizer + IconRenderer *IconRenderer +} + +// TableStyle defines the visual style of a table +type TableStyle struct { + Border bool + HeaderSeparator bool + RowSeparator bool + Padding int + BorderColor lipgloss.Style + HeaderColor lipgloss.Style + EvenRowColor lipgloss.Style + OddRowColor lipgloss.Style +} + +// NewTable creates a new table with lipgloss styling +func NewTable(colorizer *Colorizer, iconRenderer *IconRenderer) *Table { + return &Table{ + Colorizer: colorizer, + IconRenderer: iconRenderer, + Style: TableStyle{ + Border: true, + HeaderSeparator: true, + RowSeparator: false, + Padding: 1, + BorderColor: tableBorderStyle, + HeaderColor: tableHeaderStyle, + EvenRowColor: tableRowEvenStyle, + OddRowColor: tableRowOddStyle, + }, + } +} + +// AddColumn adds a column to the table +func (t *Table) AddColumn(header string, width int, alignment string) { + style := lipgloss.NewStyle() + headerStyle := tableHeaderStyle + + t.Columns = append(t.Columns, TableColumn{ + Header: header, + Width: width, + Alignment: alignment, + Style: style, + HeaderStyle: headerStyle, + }) +} + +// AddRow adds a row to the table +func (t *Table) AddRow(cells ...string) { + row := TableRow{Style: lipgloss.NewStyle()} + + for _, cell := range cells { + row.Cells = append(row.Cells, TableCell{ + Text: cell, + Style: lipgloss.NewStyle(), + Align: "left", + }) + } + + t.Rows = append(t.Rows, row) +} + +// AddStyledRow adds a styled row to the table +func (t *Table) AddStyledRow(style lipgloss.Style, cells ...string) { + row := TableRow{Style: style} + + for _, cell := range cells { + row.Cells = append(row.Cells, TableCell{ + Text: cell, + Style: lipgloss.NewStyle(), + Align: "left", + }) + } + + t.Rows = append(t.Rows, row) +} + +// AddStyledCellRow adds a row with individually styled cells +func (t *Table) AddStyledCellRow(cells ...TableCell) { + row := TableRow{Style: lipgloss.NewStyle()} + row.Cells = cells + t.Rows = append(t.Rows, row) +} + +// Render renders the table as a string using lipgloss +func (t *Table) Render() string { + if len(t.Columns) == 0 { + return "" + } + + var rows []string + + // Render header + if t.Style.Border { + rows = append(rows, t.renderBorderLine("top")) + } + + headerRow := t.renderHeader() + rows = append(rows, headerRow) + + if t.Style.HeaderSeparator { + rows = append(rows, t.renderBorderLine("header")) + } + + // Render rows + for i, row := range t.Rows { + if t.Style.RowSeparator && i > 0 { + rows = append(rows, t.renderBorderLine("row")) + } + + rowStyle := t.Style.EvenRowColor + if i%2 == 1 { + rowStyle = t.Style.OddRowColor + } + + renderedRow := t.renderRow(row, rowStyle) + rows = append(rows, renderedRow) + } + + // Render bottom border + if t.Style.Border { + rows = append(rows, t.renderBorderLine("bottom")) + } + + return strings.Join(rows, "\n") +} + +// renderHeader renders the table header +func (t *Table) renderHeader() string { + var cells []string + + for _, col := range t.Columns { + paddedHeader := t.padText(col.Header, col.Width, col.Alignment) + styledHeader := col.HeaderStyle.Render(paddedHeader) + cells = append(cells, styledHeader) + } + + return t.renderRowLine(cells, t.Style.BorderColor) +} + +// renderRow renders a table row +func (t *Table) renderRow(row TableRow, defaultStyle lipgloss.Style) string { + var cells []string + + for i, cell := range row.Cells { + colStyle := defaultStyle + if i < len(t.Columns) { + colStyle = t.Columns[i].Style + } + + cellStyle := cell.Style + if cellStyle.String() == lipgloss.NewStyle().String() { + cellStyle = colStyle + } + + align := cell.Align + if align == "" && i < len(t.Columns) { + align = t.Columns[i].Alignment + } + + width := 20 + if i < len(t.Columns) { + width = t.Columns[i].Width + } + + paddedText := t.padText(cell.Text, width, align) + styledText := cellStyle.Render(paddedText) + cells = append(cells, styledText) + } + + return t.renderRowLine(cells, defaultStyle) +} + +// renderRowLine renders a line of cells +func (t *Table) renderRowLine(cells []string, borderStyle lipgloss.Style) string { + if t.Style.Border { + border := borderStyle.Render(" ") + separator := borderStyle.Render(" ") + return border + " " + strings.Join(cells, " "+separator+" ") + " " + border + } else { + return strings.Join(cells, " ") + } +} + +// renderBorderLine renders a border line +func (t *Table) renderBorderLine(position string) string { + if !t.Style.Border { + return "" + } + + var sections []string + + for _, col := range t.Columns { + width := col.Width + 2 // Account for padding + if t.Style.Padding > 0 { + width += t.Style.Padding * 2 + } + sections = append(sections, strings.Repeat("=", width)) + } + + border := t.Style.BorderColor.Render("+") + separator := t.Style.BorderColor.Render("+") + + return border + strings.Join(sections, separator) + border +} + +// padText pads text to the specified width with alignment +func (t *Table) padText(text string, width int, alignment string) string { + if len(text) >= width { + return text[:width] + } + + padding := width - len(text) + + switch alignment { + case "right": + return strings.Repeat(" ", padding) + text + case "center": + leftPad := padding / 2 + rightPad := padding - leftPad + return strings.Repeat(" ", leftPad) + text + strings.Repeat(" ", rightPad) + default: // left + return text + strings.Repeat(" ", padding) + } +} + +// Convenience methods for creating common table types + +// NewServicesTable creates a table optimized for service listings with lipgloss styling +func NewServicesTable(colorizer *Colorizer, iconRenderer *IconRenderer) *Table { + table := NewTable(colorizer, iconRenderer) + table.AddColumn("", 3, "center") // Icon + table.AddColumn("Service", 15, "left") + table.AddColumn("Status", 10, "center") + table.AddColumn("Version", 10, "center") + table.AddColumn("CPU", 6, "right") + table.AddColumn("Memory", 8, "right") + table.AddColumn("Uptime", 8, "right") + return table +} + +// NewSubscribersTable creates a table optimized for subscriber listings with lipgloss styling +func NewSubscribersTable(colorizer *Colorizer, iconRenderer *IconRenderer) *Table { + table := NewTable(colorizer, iconRenderer) + table.AddColumn("", 3, "center") // Icon + table.AddColumn("IMSI", 15, "left") + table.AddColumn("Name", 18, "left") + table.AddColumn("Status", 10, "center") + table.AddColumn("Balance", 10, "right") + table.AddColumn("Plan", 10, "left") + table.AddColumn("Last Activity", 12, "center") + return table +} + +// NewMetricsTable creates a table optimized for metrics display with lipgloss styling +func NewMetricsTable(colorizer *Colorizer, iconRenderer *IconRenderer) *Table { + table := NewTable(colorizer, iconRenderer) + table.AddColumn("", 3, "center") // Icon + table.AddColumn("Metric", 20, "left") + table.AddColumn("Value", 12, "right") + table.AddColumn("Status", 10, "center") + table.AddColumn("Threshold", 12, "right") + return table +} + +// NewSimpleTable creates a simple table with basic columns and lipgloss styling +func NewSimpleTable(colorizer *Colorizer, iconRenderer *IconRenderer, headers ...string) *Table { + table := NewTable(colorizer, iconRenderer) + + for _, header := range headers { + table.AddColumn(header, 15, "left") + } + + return table +} + +// NewStyledTable creates a table with custom styling +func NewStyledTable(colorizer *Colorizer, iconRenderer *IconRenderer, style TableStyle) *Table { + table := &Table{ + Colorizer: colorizer, + IconRenderer: iconRenderer, + Style: style, + } + return table +}