81 lines
2.6 KiB
Go
81 lines
2.6 KiB
Go
// Package ui holds the CLI's visual vocabulary: one palette, one set of status
|
|
// badges, and one keymap, so every screen looks and behaves the same way.
|
|
package ui
|
|
|
|
import (
|
|
"image/color"
|
|
"strings"
|
|
|
|
"charm.land/lipgloss/v2"
|
|
"github.com/charmbracelet/x/ansi"
|
|
)
|
|
|
|
// The palette is intentionally small. Colour carries meaning here — green is
|
|
// healthy, red is broken, amber is in flight, cyan is local — so introducing
|
|
// extra hues would dilute the signal rather than add information.
|
|
var (
|
|
Accent = lipgloss.Color("212")
|
|
Success = lipgloss.Color("42")
|
|
Warning = lipgloss.Color("214")
|
|
Danger = lipgloss.Color("203")
|
|
Info = lipgloss.Color("81")
|
|
Muted = lipgloss.Color("241")
|
|
Faint = lipgloss.Color("238")
|
|
Text = lipgloss.Color("252")
|
|
Bright = lipgloss.Color("230")
|
|
Ink = lipgloss.Color("235")
|
|
)
|
|
|
|
var (
|
|
Title = lipgloss.NewStyle().Bold(true).Foreground(Accent)
|
|
Heading = lipgloss.NewStyle().Bold(true).Foreground(Bright)
|
|
Help = lipgloss.NewStyle().Foreground(Muted)
|
|
Dim = lipgloss.NewStyle().Foreground(Faint)
|
|
Body = lipgloss.NewStyle().Foreground(Text)
|
|
OK = lipgloss.NewStyle().Foreground(Success)
|
|
Alert = lipgloss.NewStyle().Foreground(Danger)
|
|
Pending = lipgloss.NewStyle().Foreground(Warning)
|
|
Local = lipgloss.NewStyle().Foreground(Info)
|
|
Selected = lipgloss.NewStyle().Bold(true).Foreground(Bright).Background(lipgloss.Color("57"))
|
|
)
|
|
|
|
// Panel frames the sidebar and overlays.
|
|
func Panel(title string, focused bool) lipgloss.Style {
|
|
var border color.Color = Faint
|
|
if focused {
|
|
border = Accent
|
|
}
|
|
return lipgloss.NewStyle().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(border).
|
|
Padding(0, 1)
|
|
}
|
|
|
|
// Rule draws a labelled separator across the given width.
|
|
func Rule(label string, width int) string {
|
|
if width < 8 {
|
|
return Dim.Render(Truncate(label, width))
|
|
}
|
|
prefix := "── " + label + " "
|
|
prefixWidth := lipgloss.Width(prefix)
|
|
if prefixWidth >= width {
|
|
return Dim.Render(Truncate(prefix, width))
|
|
}
|
|
fill := width - prefixWidth
|
|
return Dim.Render(prefix + strings.Repeat("─", fill))
|
|
}
|
|
|
|
// Blank reports whether a line would render as empty. Build tools emit lines
|
|
// that hold nothing but a colour reset; they are not empty as strings but they
|
|
// occupy a row for no reason, which turns a log panel into double-spaced text.
|
|
func Blank(line string) bool { return lipgloss.Width(strings.TrimSpace(line)) == 0 }
|
|
|
|
// Truncate shortens a string to display width, marking the cut with an ellipsis.
|
|
// It handles ANSI escape sequences and multi-byte runes correctly.
|
|
func Truncate(value string, width int) string {
|
|
if width <= 0 {
|
|
return ""
|
|
}
|
|
return ansi.Truncate(value, width, "…")
|
|
}
|