150 lines
3.5 KiB
Go
150 lines
3.5 KiB
Go
package internal
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
// ────────────────────────────────
|
|
// STYLES
|
|
// ────────────────────────────────
|
|
|
|
type Styles struct {
|
|
Title lipgloss.Style
|
|
Box lipgloss.Style
|
|
Active lipgloss.Style
|
|
Status lipgloss.Style
|
|
Error lipgloss.Style // NEW: for red bold error lines
|
|
}
|
|
|
|
func NewStyles() Styles {
|
|
border := lipgloss.RoundedBorder()
|
|
return Styles{
|
|
Title: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12")),
|
|
Box: lipgloss.NewStyle().Border(border).Padding(0, 1).MarginRight(1),
|
|
Active: lipgloss.NewStyle().
|
|
Border(border).
|
|
BorderForeground(lipgloss.Color("#FA8072")). // Not pink, its Salmon obviously
|
|
Padding(0, 1).
|
|
MarginRight(1),
|
|
Status: lipgloss.NewStyle().Foreground(lipgloss.Color("8")).MarginTop(1),
|
|
}
|
|
}
|
|
|
|
// ────────────────────────────────
|
|
// GENERIC LIST COLUMN (SCROLLABLE)
|
|
// ────────────────────────────────
|
|
|
|
type renderer[T any] func(T) string
|
|
|
|
type ListColumn[T any] struct {
|
|
title string
|
|
items []T
|
|
selected int
|
|
scroll int
|
|
width int
|
|
height int
|
|
render renderer[T]
|
|
}
|
|
|
|
func NewListColumn[T any](title string, r renderer[T]) *ListColumn[T] {
|
|
return &ListColumn[T]{title: title, render: r, width: 30, height: 20}
|
|
}
|
|
|
|
func (c *ListColumn[T]) SetItems(items []T) {
|
|
c.items = items
|
|
c.selected = 0
|
|
c.scroll = 0
|
|
}
|
|
|
|
func (c *ListColumn[T]) SetTitle(title string) { c.title = title }
|
|
|
|
func (c *ListColumn[T]) SetWidth(w int) {
|
|
// w is the total width the app wants to allocate to the box.
|
|
// Subtract 4 for border (2) + padding (2) to get interior content width.
|
|
if w < 4 {
|
|
c.width = 0
|
|
return
|
|
}
|
|
c.width = w - 4
|
|
}
|
|
|
|
func (c *ListColumn[T]) SetHeight(h int) {
|
|
if h > 5 {
|
|
c.height = h - 5
|
|
}
|
|
}
|
|
|
|
func (c *ListColumn[T]) CursorUp() {
|
|
if c.selected > 0 {
|
|
c.selected--
|
|
}
|
|
if c.selected < c.scroll {
|
|
c.scroll = c.selected
|
|
}
|
|
}
|
|
|
|
func (c *ListColumn[T]) CursorDown() {
|
|
if c.selected < len(c.items)-1 {
|
|
c.selected++
|
|
}
|
|
if c.selected >= c.scroll+c.height {
|
|
c.scroll = c.selected - c.height + 1
|
|
}
|
|
}
|
|
|
|
func (c *ListColumn[T]) Selected() (T, bool) {
|
|
var zero T
|
|
if len(c.items) == 0 {
|
|
return zero, false
|
|
}
|
|
return c.items[c.selected], true
|
|
}
|
|
|
|
func (c *ListColumn[T]) View(styles Styles, focused bool) string {
|
|
box := styles.Box
|
|
if focused {
|
|
box = styles.Active
|
|
}
|
|
|
|
head := styles.Title.Render(c.title)
|
|
lines := []string{}
|
|
|
|
if len(c.items) == 0 {
|
|
lines = append(lines, "(no items)")
|
|
} else {
|
|
start := c.scroll
|
|
end := start + c.height
|
|
if end > len(c.items) {
|
|
end = len(c.items)
|
|
}
|
|
for i := start; i < end; i++ {
|
|
cursor := " "
|
|
lineText := c.render(c.items[i])
|
|
if i == c.selected {
|
|
cursor = "▸ "
|
|
lineText = lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("#FA8072")). // Not pink, its Salmon obviously
|
|
Bold(true).
|
|
Render(lineText)
|
|
}
|
|
line := fmt.Sprintf("%s%s", cursor, lineText)
|
|
if len(line) > c.width && c.width > 3 {
|
|
line = line[:c.width-3] + "…"
|
|
}
|
|
lines = append(lines, line)
|
|
}
|
|
}
|
|
|
|
// Fill remaining lines if fewer than height
|
|
for len(lines) < c.height {
|
|
lines = append(lines, "")
|
|
}
|
|
|
|
content := strings.Join(lines, "\n")
|
|
// IMPORTANT: width = interior content width + 4 (border+padding)
|
|
return box.Width(c.width + 4).Render(head + "\n" + content)
|
|
}
|