Files
proton-vpn-tui/internal/ui/settings/delegate.go
T
2026-08-27 23:14:56 +02:00

150 lines
3.8 KiB
Go

package settings
import (
"fmt"
"image/color"
"io"
"strings"
"charm.land/bubbles/v2/list"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/charmbracelet/x/ansi"
"github.com/anotherhadi/ilovetui/style"
"github.com/anotherhadi/proton-vpn-tui/internal/backend"
"github.com/anotherhadi/proton-vpn-tui/internal/icons"
)
func clampToWidth(s string, width int) string {
if width <= 0 {
return s
}
return ansi.Truncate(s, width, "…")
}
type delegate struct{}
func (d delegate) Height() int { return 2 }
func (d delegate) Spacing() int { return 1 }
func (d delegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil }
func (d delegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
it, ok := listItem.(item)
if !ok {
return
}
selected := index == m.Index()
var titleStyle, descStyle, valueStyle lipgloss.Style
if selected {
titleStyle = lipgloss.NewStyle().
Border(lipgloss.NormalBorder(), false, false, false, true).
BorderForeground(style.S.Primary).
Foreground(style.S.Primary).
Bold(true).
PaddingLeft(1)
descStyle = lipgloss.NewStyle().
Border(lipgloss.NormalBorder(), false, false, false, true).
BorderForeground(style.S.Primary).
Foreground(style.S.Muted).
PaddingLeft(1)
valueStyle = lipgloss.NewStyle().Foreground(style.S.Success).Bold(true)
} else {
titleStyle = lipgloss.NewStyle().Foreground(style.S.Text).PaddingLeft(2)
descStyle = lipgloss.NewStyle().Foreground(style.S.Muted).PaddingLeft(2)
valueStyle = lipgloss.NewStyle().Foreground(style.S.Success)
}
value := renderCycleValue(valueStyle.Render(humanizeKebab(it.setting.Value)))
if isOnOffSetting(it.setting) {
value = renderToggle(it.setting.Value == "on")
}
width := m.Width()
title := titleStyle.Render(humanizeKebab(it.key))
line1 := padLine(justifyBetween(title, value, width), width)
line2 := padLine(descStyle.Render(it.setting.Description), width)
fmt.Fprintf(w, "%s\n%s", line1, line2)
}
func justifyBetween(left, right string, width int) string {
gap := width - lipgloss.Width(left) - lipgloss.Width(right)
if gap < 1 {
gap = 1
}
return left + strings.Repeat(" ", gap) + right
}
func padLine(s string, width int) string {
s = clampToWidth(s, width)
if pad := width - lipgloss.Width(s); pad > 0 {
s += strings.Repeat(" ", pad)
}
return s
}
func isOnOffSetting(s backend.Setting) bool {
if len(s.Options) != 2 {
return false
}
var hasOn, hasOff bool
for _, o := range s.Options {
hasOn = hasOn || o.ID == "on"
hasOff = hasOff || o.ID == "off"
}
return hasOn && hasOff
}
func renderCycleValue(value string) string {
arrow := lipgloss.NewStyle().Foreground(style.S.Muted)
return arrow.Render("< ") + value + arrow.Render(" >")
}
func renderToggle(on bool) string {
if on {
return renderBadge(" ●", style.S.Success)
}
return renderBadge("● ", style.S.Subtle)
}
func renderBadge(label string, fill color.Color) string {
body := lipgloss.NewStyle().
Background(fill).
Foreground(style.S.Background).
Bold(true).
Render(label)
if icons.I.PillLeft == "" && icons.I.PillRight == "" {
return body
}
cap := lipgloss.NewStyle().Foreground(fill).Background(style.S.Background)
return cap.Render(icons.I.PillLeft) + body + cap.Render(icons.I.PillRight)
}
var settingWordOverrides = map[string]string{
"nat": "NAT",
"dns": "DNS",
"vpn": "VPN",
"ip": "IP",
"ipv6": "IPv6",
"netshield": "NetShield",
}
func humanizeKebab(s string) string {
words := strings.Split(s, "-")
for i, w := range words {
if override, ok := settingWordOverrides[strings.ToLower(w)]; ok {
words[i] = override
continue
}
if w == "" {
continue
}
words[i] = strings.ToUpper(w[:1]) + w[1:]
}
return strings.Join(words, " ")
}