408681c8e4
- Add Theme struct with 3 predefined themes (dark/light/druntime) - Add Profile struct for named configuration profiles - Add ErrorBanner with severity levels and auto-dismiss - Add unit tests for theme, error banner, and models - Update CHANGELOG.md and PROJECT_STATE.md
138 lines
3.1 KiB
Go
138 lines
3.1 KiB
Go
package tui
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/charmbracelet/lipgloss"
|
||
)
|
||
|
||
// ErrorSeverity indicates the level of an error message
|
||
type ErrorSeverity int
|
||
|
||
const (
|
||
SevError ErrorSeverity = iota
|
||
SevWarning
|
||
SevInfo
|
||
)
|
||
|
||
// ErrorBanner displays a structured error message with title, details, and hints
|
||
type ErrorBanner struct {
|
||
Title string
|
||
Detail string
|
||
Hints []string
|
||
Severity ErrorSeverity
|
||
AutoDismiss bool
|
||
DismissAfter time.Duration
|
||
createdAt time.Time
|
||
visible bool
|
||
}
|
||
|
||
// NewErrorBanner creates a new error banner with the given severity
|
||
func NewErrorBanner(severity ErrorSeverity) *ErrorBanner {
|
||
return &ErrorBanner{
|
||
Severity: severity,
|
||
AutoDismiss: true,
|
||
DismissAfter: 5 * time.Second,
|
||
visible: false,
|
||
}
|
||
}
|
||
|
||
// Show displays the error banner with the given message
|
||
func (b *ErrorBanner) Show(title, detail string, hints ...string) {
|
||
b.Title = title
|
||
b.Detail = detail
|
||
b.Hints = hints
|
||
b.visible = true
|
||
b.createdAt = time.Now()
|
||
}
|
||
|
||
// Hide hides the error banner
|
||
func (b *ErrorBanner) Hide() {
|
||
b.visible = false
|
||
}
|
||
|
||
// IsVisible returns whether the banner is currently visible
|
||
func (b *ErrorBanner) IsVisible() bool {
|
||
return b.visible
|
||
}
|
||
|
||
// Update checks if auto-dismiss time has elapsed
|
||
func (b *ErrorBanner) Update() {
|
||
if b.visible && b.AutoDismiss && time.Since(b.createdAt) > b.DismissAfter {
|
||
b.visible = false
|
||
}
|
||
}
|
||
|
||
// View renders the error banner
|
||
func (b *ErrorBanner) View(width int) string {
|
||
if !b.visible {
|
||
return ""
|
||
}
|
||
|
||
var (
|
||
titleStyle lipgloss.Style
|
||
borderColor lipgloss.Color
|
||
prefix string
|
||
)
|
||
|
||
switch b.Severity {
|
||
case SevError:
|
||
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#ea6962")).Bold(true)
|
||
borderColor = lipgloss.Color("#ea6962")
|
||
prefix = "✖"
|
||
case SevWarning:
|
||
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#d8a657")).Bold(true)
|
||
borderColor = lipgloss.Color("#d8a657")
|
||
prefix = "⚠"
|
||
case SevInfo:
|
||
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#7daea3")).Bold(true)
|
||
borderColor = lipgloss.Color("#7daea3")
|
||
prefix = "ℹ"
|
||
}
|
||
|
||
var sb strings.Builder
|
||
|
||
// Title line with prefix
|
||
sb.WriteString(titleStyle.Render(fmt.Sprintf("%s %s", prefix, b.Title)))
|
||
|
||
// Detail line
|
||
if b.Detail != "" {
|
||
sb.WriteString("\n")
|
||
sb.WriteString(strings.Repeat(" ", len(prefix)+1))
|
||
detailStyle := lipgloss.NewStyle().Foreground(activeTheme.Fg)
|
||
sb.WriteString(detailStyle.Render(b.Detail))
|
||
}
|
||
|
||
// Hints
|
||
if len(b.Hints) > 0 {
|
||
sb.WriteString("\n")
|
||
sb.WriteString(strings.Repeat(" ", len(prefix)+1))
|
||
hintStyle := lipgloss.NewStyle().Foreground(activeTheme.FgMute)
|
||
sb.WriteString(hintStyle.Render("Hints:"))
|
||
for i, hint := range b.Hints {
|
||
sb.WriteString("\n")
|
||
sb.WriteString(strings.Repeat(" ", len(prefix)+2))
|
||
sb.WriteString(hintStyle.Render(fmt.Sprintf("%d. %s", i+1, hint)))
|
||
}
|
||
}
|
||
|
||
// Wrap in a styled box
|
||
borderStyle := lipgloss.NewStyle().
|
||
Border(lipgloss.RoundedBorder()).
|
||
BorderForeground(borderColor).
|
||
Padding(0, 1).
|
||
Width(min(width-2, 80))
|
||
|
||
return borderStyle.Render(sb.String())
|
||
}
|
||
|
||
// min returns the smaller of two integers
|
||
func min(a, b int) int {
|
||
if a < b {
|
||
return a
|
||
}
|
||
return b
|
||
}
|