Files
2026-08-27 23:14:56 +02:00

122 lines
3.5 KiB
Go

package app
import (
"fmt"
"strings"
"charm.land/lipgloss/v2"
"github.com/charmbracelet/x/ansi"
zone "github.com/lrstanley/bubblezone/v2"
"github.com/anotherhadi/ilovetui/style"
"github.com/anotherhadi/proton-vpn-tui/internal/icons"
)
func (m Model) renderHeader() string {
title := headerStyle.Render("ProtonVPN")
if style.S.NerdFonts {
title = icons.I.Vpn + title
}
lines := []string{
title,
"",
m.renderStatusLine(),
m.renderModesLine(),
}
if searchLine := m.renderSearchLine(); searchLine != "" {
lines = append(lines, searchLine)
}
lines = append(lines, "", m.renderColumnHeader())
for i, l := range lines {
lines[i] = clampToWidth(l, m.width)
}
return strings.Join(lines, "\n")
}
func (m Model) renderColumnHeader() string {
indent, expandIcon := "", " "
prefixWidth := lipgloss.Width(indent) + lipgloss.Width(expandIcon) + 1
infoW, gaps := infoColumnLayout(m.showServersColumn())
nameWidth := max(m.width-boxOverhead-prefixWidth-infoW-loadColWidth-gaps, minNameWidth)
hdrStyle := lipgloss.NewStyle().Foreground(style.S.Subtle).Bold(true)
line := " " + indent + expandIcon + " " + hdrStyle.Render(fixedWidth("Name", nameWidth))
if m.showServersColumn() {
line += " " + hdrStyle.Render(fixedWidth("Servers", infoColWidth))
}
return line + " " + hdrStyle.Render(fixedWidth("Load", loadColWidth))
}
func oneLine(s string) string {
return strings.Join(strings.Fields(s), " ")
}
func clampToWidth(s string, width int) string {
if width <= 0 {
return s
}
return ansi.Truncate(s, width, "…")
}
func (m Model) renderStatusLine() string {
label := infoLabelStyle.Render("Status")
if m.connecting {
return label + primaryStyle.Render(icons.I.Connecting+" connecting…")
}
if m.disconnecting {
return label + mutedStyle.Render(icons.I.Loading+" loading…")
}
if m.statusErr != nil {
return label + warnStyle.Render(oneLine(m.statusErr.Error()))
}
if !m.statusLoaded {
if m.quickConnected {
return label + mutedStyle.Render(icons.I.Connected+" connected, "+icons.I.Loading+" loading…")
}
return label + mutedStyle.Render(icons.I.Loading+" loading…")
}
if !m.status.Connected {
return label + mutedStyle.Render(icons.I.Disconnected+" disconnected")
}
loc := m.status.Server
if m.status.Location != "" {
loc = fmt.Sprintf("%s (%s)", m.status.Server, m.status.Location)
}
text := fmt.Sprintf("%s connected: %s", icons.I.Connected, loc)
if m.status.Load > 0 {
text += fmt.Sprintf(" %d%% load", m.status.Load)
}
return label + connectedStyle.Render(text)
}
func (m Model) renderSearchLine() string {
if !m.searching && strings.TrimSpace(m.searchInput.Value()) == "" {
return ""
}
return infoLabelStyle.Render("Search") + m.searchInput.View()
}
func (m Model) renderModesLine() string {
label := infoLabelStyle.Render("Mode")
parts := []string{
zone.Mark(zoneBadgeFastest, modeBadge(icons.I.Fastest+" fastest", m.prefer == "fastest")),
zone.Mark(zoneBadgeRandom, modeBadge(icons.I.Random+" random", m.prefer == "random")),
zone.Mark(zoneBadgeSecureCore, modeBadge(icons.I.SecureCore+" secure core", m.filters.SecureCore)),
zone.Mark(zoneBadgeTor, modeBadge(icons.I.Tor+" tor", m.filters.Tor)),
zone.Mark(zoneBadgeP2P, modeBadge(icons.I.P2p+" p2p", m.filters.P2P)),
zone.Mark(zoneBadgeFree, modeBadge(icons.I.Free+" free", m.filters.Free)),
}
return label + strings.Join(parts, mutedStyle.Render(" "))
}
func modeBadge(text string, active bool) string {
if active {
return primaryStyle.Bold(true).Render(text)
}
return mutedStyle.Render(text)
}