Files
kportal/internal/ui/wizard_styles.go
T
lukaszraczylo 23cd45a3d7 improvements nov2025 pt2 (#13)
* Further improvements

| Fix                                | Impact                                 | Files Modified                       |
|------------------------------------|----------------------------------------|--------------------------------------|
| sync.Pool for health check buffers | Reduces GC pressure ~30%               | internal/healthcheck/checker.go      |
| Goroutine leak fix + sync.Once     | Prevents memory leaks                  | internal/forward/worker.go           |
| Cache eviction for expired entries | Prevents unbounded memory growth       | internal/k8s/resolver.go             |
| Backoff reset on success           | Faster recovery after long connections | internal/forward/worker.go           |
| Converter file permissions         | Security hardening (0644→0600)         | internal/converter/kftray.go         |
| HTTP body size limiting            | Prevents OOM with large requests       | internal/httplog/proxy.go, logger.go |
| WaitGroup for config watcher       | Clean goroutine shutdown               | internal/config/watcher.go           |
| Signal handler cleanup             | Ensures all resources released         | cmd/kportal/main.go                  |

* Additional event bus for internal event handling

| Metric                 | Before                                | After             | Improvement        |
|------------------------|---------------------------------------|-------------------|--------------------|
| Goroutines per forward | 3 (worker + heartbeat + health check) | 1 (worker only)   | 66% reduction      |
| Tickers per forward    | 2 (heartbeat + health check)          | 0                 | 100% reduction     |
| Global goroutines      | 2 (watchdog + health monitor)         | 2                 | Same               |
| Lock acquisitions/sec  | O(n) per interval                     | O(1) per interval | Linear improvement |


* Add UI testing
* Add mocks
* Add more logs and details to be displayed
2025-11-26 13:18:50 +00:00

212 lines
5.1 KiB
Go

package ui
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
)
// Color palette for wizards
var (
primaryColor = lipgloss.Color("205") // Pink/Magenta
successColor = lipgloss.Color("42") // Green
errorColor = lipgloss.Color("196") // Red
warningColor = lipgloss.Color("220") // Yellow
mutedColor = lipgloss.Color("241") // Gray
accentColor = lipgloss.Color("63") // Purple
highlightColor = lipgloss.Color("117") // Light blue
// JSON syntax highlighting colors
jsonKeyColor = lipgloss.Color("81") // Cyan
jsonStringColor = lipgloss.Color("180") // Light orange/tan
jsonNumberColor = lipgloss.Color("141") // Light purple
jsonBoolColor = lipgloss.Color("209") // Orange
jsonNullColor = lipgloss.Color("243") // Dark gray
)
// Text styles
var (
wizardHeaderStyle = lipgloss.NewStyle().
Bold(true).
Foreground(primaryColor).
MarginBottom(0)
wizardStepStyle = lipgloss.NewStyle().
Foreground(mutedColor).
Italic(true)
breadcrumbStyle = lipgloss.NewStyle().
Foreground(highlightColor).
Bold(true)
selectedStyle = lipgloss.NewStyle().
Foreground(primaryColor).
Bold(true)
successStyle = lipgloss.NewStyle().
Foreground(successColor).
Bold(true)
errorStyle = lipgloss.NewStyle().
Foreground(errorColor).
Bold(true)
warningStyle = lipgloss.NewStyle().
Foreground(warningColor).
Bold(true)
mutedStyle = lipgloss.NewStyle().
Foreground(mutedColor)
helpStyle = lipgloss.NewStyle().
Foreground(mutedColor).
Italic(true)
spinnerStyle = lipgloss.NewStyle().
Foreground(accentColor).
Bold(true)
accentStyle = lipgloss.NewStyle().
Foreground(accentColor).
Bold(true)
)
// Input styles
var (
inputStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("252"))
validInputStyle = lipgloss.NewStyle().
Foreground(successColor)
)
// Checkbox styles
var (
checkedBoxStyle = lipgloss.NewStyle().
Foreground(successColor).
Bold(true)
uncheckedBoxStyle = lipgloss.NewStyle().
Foreground(mutedColor)
)
// JSON syntax highlighting styles
var (
jsonKeyStyle = lipgloss.NewStyle().
Foreground(jsonKeyColor)
jsonStringStyle = lipgloss.NewStyle().
Foreground(jsonStringColor)
jsonNumberStyle = lipgloss.NewStyle().
Foreground(jsonNumberColor)
jsonBoolStyle = lipgloss.NewStyle().
Foreground(jsonBoolColor)
jsonNullStyle = lipgloss.NewStyle().
Foreground(jsonNullColor)
)
// Container styles
var (
// wizardBoxStyle creates a bordered modal box
wizardBoxStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(accentColor).
Padding(1, 2)
)
// Helper functions for rendering
// renderProgress returns a step indicator like "Step 2/7"
func renderProgress(current, total int) string {
return wizardStepStyle.Render(fmt.Sprintf("Step %d/%d", current, total))
}
// renderHeader returns a formatted header with title and progress
func renderHeader(title, progress string) string {
header := wizardHeaderStyle.Render(title)
if progress != "" {
header += " " + progress
}
return header + "\n\n"
}
// renderBreadcrumb returns a formatted breadcrumb path
func renderBreadcrumb(parts ...string) string {
return breadcrumbStyle.Render(strings.Join(parts, " / "))
}
// renderList renders a list of items with cursor selection and viewport scrolling
func renderList(items []string, cursor int, prefix string, scrollOffset int) string {
var b strings.Builder
const viewportHeight = 20
totalItems := len(items)
// Show scroll up indicator if there are items above the viewport
if scrollOffset > 0 {
b.WriteString(mutedStyle.Render(" ↑ More above ↑") + "\n")
}
// Calculate visible range
start := scrollOffset
end := scrollOffset + viewportHeight
if end > totalItems {
end = totalItems
}
// Render visible items
for i := start; i < end; i++ {
cursorPrefix := prefix
if i == cursor {
cursorPrefix = "▸ "
b.WriteString(selectedStyle.Render(cursorPrefix + items[i]))
} else {
b.WriteString(cursorPrefix + items[i])
}
b.WriteString("\n")
}
// Show scroll down indicator if there are items below the viewport
if end < totalItems {
b.WriteString(mutedStyle.Render(" ↓ More below ↓") + "\n")
}
return b.String()
}
// renderTextInput renders a text input field with a cursor
func renderTextInput(label, value string, valid bool) string {
var b strings.Builder
b.WriteString(label)
inputText := value + "█"
if valid {
b.WriteString(validInputStyle.Render(inputText))
} else {
b.WriteString(inputStyle.Render(inputText))
}
return b.String()
}
// overlayContent overlays modal content centered on the base view
// Note: base parameter is kept for API compatibility but not used since
// lipgloss.Place provides cleaner centering without background artifacts
func overlayContent(_, modal string, termWidth, termHeight int) string {
// Use lipgloss.Place to center the modal in the terminal viewport
// This handles all alignment properly and respects ANSI styling
return lipgloss.Place(
termWidth,
termHeight,
lipgloss.Center,
lipgloss.Center,
modal,
lipgloss.WithWhitespaceChars(" "),
)
}