Standardize TUI formatting and styling across all screens

This commit is contained in:
Calmcacil
2026-01-12 23:41:07 +01:00
parent 0476f1e227
commit f154c7ff69
8 changed files with 159 additions and 152 deletions

View File

@@ -2,6 +2,7 @@ package screens
import (
"github.com/calmcacil/wg-admin/internal/tui/components"
"github.com/calmcacil/wg-admin/internal/tui/theme"
"github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
@@ -41,55 +42,41 @@ func (s *HelpScreen) View() string {
// Breadcrumb: Help
breadcrumb := components.RenderBreadcrumb([]components.BreadcrumbItem{{Label: "Help", ID: "help"}})
// Styles
// Styles using theme
borderStyle := lipgloss.NewStyle().
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("62")).
BorderForeground(theme.StyleBorder).
Padding(1, 2)
headerStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("62")).
Bold(true).
MarginBottom(1)
categoryStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("226")).
Bold(true).
MarginTop(1).
MarginBottom(0)
keyStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("63")).
Bold(true).
Width(12)
keyStyle := theme.StyleHelpKey.Width(12)
descStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("250"))
// Header
header := headerStyle.Render("Keyboard Shortcuts")
header := theme.StyleTitle.MarginBottom(1).Render("Keyboard Shortcuts")
// Shortcut groups
navigationGroup := categoryStyle.Render("Navigation") + "\n" +
navigationGroup := theme.StyleWarning.Bold(true).MarginTop(1).MarginBottom(0).Render("Navigation") + "\n" +
keyStyle.Render("j / ↓") + descStyle.Render("Move down") + "\n" +
keyStyle.Render("k / ↑") + descStyle.Render("Move up") + "\n" +
keyStyle.Render("← →") + descStyle.Render("Move left/right") + "\n" +
keyStyle.Render("Enter") + descStyle.Render("Select") + "\n" +
keyStyle.Render("Esc") + descStyle.Render("Go back")
actionsGroup := categoryStyle.Render("Actions") + "\n" +
actionsGroup := theme.StyleWarning.Bold(true).MarginTop(1).MarginBottom(0).Render("Actions") + "\n" +
keyStyle.Render("a") + descStyle.Render("Add client") + "\n" +
keyStyle.Render("d") + descStyle.Render("Delete client") + "\n" +
keyStyle.Render("Q") + descStyle.Render("Show QR code") + "\n" +
keyStyle.Render("r") + descStyle.Render("Refresh list") + "\n" +
keyStyle.Render("l") + descStyle.Render("List view")
otherGroup := categoryStyle.Render("Other") + "\n" +
otherGroup := theme.StyleWarning.Bold(true).MarginTop(1).MarginBottom(0).Render("Other") + "\n" +
keyStyle.Render("?") + descStyle.Render("Show this help") + "\n" +
keyStyle.Render("/") + descStyle.Render("Search") + "\n" +
keyStyle.Render("q") + descStyle.Render("Quit")
copyGroup := categoryStyle.Render("Text Selection & Copy") + "\n" +
copyGroup := theme.StyleWarning.Bold(true).MarginTop(1).MarginBottom(0).Render("Text Selection & Copy") + "\n" +
keyStyle.Render("SHIFT+drag") + descStyle.Render("Select text") + "\n" +
keyStyle.Render("Ctrl+Shift+C") + descStyle.Render("Copy (Linux)") + "\n" +
keyStyle.Render("Cmd+C") + descStyle.Render("Copy (macOS)")
@@ -110,10 +97,7 @@ func (s *HelpScreen) View() string {
content := lipgloss.JoinHorizontal(lipgloss.Top, leftColumn, " ", rightColumn)
// Footer
footerStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("241")).
MarginTop(1)
footer := footerStyle.Render("Press q or Esc to return")
footer := theme.StyleMuted.MarginTop(1).Render("Press q or Esc to return")
// Combine all
return breadcrumb + "\n\n" + borderStyle.Render(

View File

@@ -28,8 +28,9 @@ type ListScreen struct {
// ClientWithStatus wraps a client with its connection status
type ClientWithStatus struct {
Client wireguard.Client
Status string
Client wireguard.Client
Status string
Quality string
}
// NewListScreen creates a new list screen
@@ -204,7 +205,6 @@ func (s *ListScreen) View() string {
Foreground(lipgloss.Color("241")).
Render("Last updated: " + timeAgo)
// Add keyboard shortcuts help
helpText := lipgloss.NewStyle().
Foreground(lipgloss.Color("63")).
@@ -225,6 +225,8 @@ func formatDuration(d time.Duration) string {
return fmt.Sprintf("%d hr ago", int(d.Hours()))
}
// loadClients loads clients from wireguard config
// loadClients loads clients from wireguard config
func (s *ListScreen) loadClients() tea.Msg {
clients, err := wireguard.ListClients()
@@ -232,16 +234,31 @@ func (s *ListScreen) loadClients() tea.Msg {
return ErrMsg{Err: err}
}
// Get status for each client
// Get all peer statuses to retrieve quality information
peerStatuses, err := wireguard.GetAllPeers()
if err != nil {
return ErrMsg{Err: err}
}
// Match clients with their peer status
clientsWithStatus := make([]ClientWithStatus, len(clients))
for i, client := range clients {
status, err := wireguard.GetClientStatus(client.PublicKey)
if err != nil {
status = wireguard.StatusDisconnected
status := wireguard.StatusDisconnected
quality := ""
// Find matching peer status
for _, peerStatus := range peerStatuses {
if peerStatus.PublicKey == client.PublicKey {
status = peerStatus.Status
quality = peerStatus.Quality
break
}
}
clientsWithStatus[i] = ClientWithStatus{
Client: client,
Status: status,
Client: client,
Status: status,
Quality: quality,
}
}
@@ -281,8 +298,11 @@ func (s *ListScreen) applyFilter() {
}
// formatStatusWithIcon formats the status with a colored circle icon
func (s *ListScreen) formatStatusWithIcon(status string) string {
func (s *ListScreen) formatStatusWithIcon(status string, quality string) string {
if status == wireguard.StatusConnected {
if quality != "" {
return lipgloss.NewStyle().Foreground(lipgloss.Color("46")).Render("●") + " " + status + " (" + quality + ")"
}
return lipgloss.NewStyle().Foreground(lipgloss.Color("46")).Render("●") + " " + status
}
return lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Render("●") + " " + status
@@ -305,11 +325,30 @@ func (s *ListScreen) buildTable() {
var rows []table.Row
for _, cws := range displayClients {
statusText := s.formatStatusWithIcon(cws.Status)
// Apply highlighting based on filter type
name := cws.Client.Name
ipv4 := cws.Client.IPv4
ipv6 := cws.Client.IPv6
status := cws.Status
if s.search.IsActive() {
switch s.search.GetFilterType() {
case components.FilterByName:
name = s.search.HighlightMatches(name)
case components.FilterByIPv4:
ipv4 = s.search.HighlightMatches(ipv4)
case components.FilterByIPv6:
ipv6 = s.search.HighlightMatches(ipv6)
case components.FilterByStatus:
status = s.search.HighlightMatches(status)
}
}
statusText := s.formatStatusWithIcon(status, "")
row := table.Row{
cws.Client.Name,
cws.Client.IPv4,
cws.Client.IPv6,
name,
ipv4,
ipv6,
statusText,
}
rows = append(rows, row)

View File

@@ -5,6 +5,7 @@ import (
"strings"
"github.com/calmcacil/wg-admin/internal/tui/components"
"github.com/calmcacil/wg-admin/internal/tui/theme"
"github.com/calmcacil/wg-admin/internal/wireguard"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
@@ -102,39 +103,22 @@ func (s *QRScreen) generateQRCode() {
// renderQR renders the QR code with styling
func (s *QRScreen) renderQR() string {
styleTitle := lipgloss.NewStyle().
Foreground(lipgloss.Color("62")).
Bold(true).
MarginBottom(1)
styleHelp := lipgloss.NewStyle().
Foreground(lipgloss.Color("241")).
MarginTop(1)
styleQR := lipgloss.NewStyle().
MarginLeft(2)
title := styleTitle.Render(fmt.Sprintf("QR Code: %s", s.clientName))
title := theme.StyleTitle.MarginBottom(1).Render(fmt.Sprintf("QR Code: %s", s.clientName))
help := "Press [f] to toggle fullscreen • Press [q/Escape] to return"
return title + "\n\n" + styleQR.Render(s.qrCode) + "\n" + styleHelp.Render(help)
return title + "\n\n" + styleQR.Render(s.qrCode) + "\n" + theme.StyleMuted.MarginTop(1).Render(help)
}
// renderError renders an error message
func (s *QRScreen) renderError() string {
styleError := lipgloss.NewStyle().
Foreground(lipgloss.Color("196")).
Bold(true)
styleHelp := lipgloss.NewStyle().
Foreground(lipgloss.Color("241")).
MarginTop(1)
title := styleError.Render("Error")
title := theme.StyleError.Bold(true).Render("Error")
message := s.errorMsg
help := "Press [q/Escape] to return"
return title + "\n\n" + message + "\n" + styleHelp.Render(help)
return title + "\n\n" + message + "\n" + theme.StyleMuted.MarginTop(1).Render(help)
}
// Messages

View File

@@ -6,6 +6,7 @@ import (
"github.com/calmcacil/wg-admin/internal/backup"
"github.com/calmcacil/wg-admin/internal/tui/components"
"github.com/calmcacil/wg-admin/internal/tui/theme"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
@@ -26,27 +27,7 @@ type RestoreScreen struct {
spinner spinner.Model
}
// Styles
var (
restoreTitleStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("62")).
Bold(true)
restoreHelpStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("63")).
MarginTop(1)
restoreSuccessStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("46")).
Bold(true)
restoreErrorStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("196")).
Bold(true)
restoreInfoStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("241")).
MarginTop(1)
restoreLoadingStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("62")).
Bold(true)
)
// No local styles - all use theme package
// NewRestoreScreen creates a new restore screen
func NewRestoreScreen() *RestoreScreen {
@@ -184,12 +165,11 @@ func (s *RestoreScreen) renderContent() string {
// Breadcrumb: Clients > Restore
breadcrumb := components.RenderBreadcrumb([]components.BreadcrumbItem{{Label: "Clients", ID: "list"}, {Label: "Restore", ID: "restore"}})
var content strings.Builder
content.WriteString(breadcrumb)
content.WriteString("\n")
content.WriteString(restoreTitleStyle.Render("Restore WireGuard Configuration"))
content.WriteString(theme.StyleTitle.Render("Restore WireGuard Configuration"))
content.WriteString("\n\n")
if len(s.backups) == 0 && !s.isRestoring && s.message == "" {
@@ -198,23 +178,23 @@ func (s *RestoreScreen) renderContent() string {
}
if s.isRestoring {
content.WriteString(restoreLoadingStyle.Render(s.spinner.View() + " Restoring from backup, please wait..."))
content.WriteString(theme.StyleTitle.Render(s.spinner.View() + " Restoring from backup, please wait..."))
return content.String()
}
if s.restoreSuccess {
content.WriteString(restoreSuccessStyle.Render("✓ " + s.message))
content.WriteString(theme.StyleSuccess.Render("✓ " + s.message))
content.WriteString("\n\n")
content.WriteString(restoreInfoStyle.Render("Press 'q' to return to client list."))
content.WriteString(theme.StyleMuted.Render("Press 'q' to return to client list."))
return content.String()
}
if s.restoreError != nil {
content.WriteString(restoreErrorStyle.Render("✗ " + s.message))
content.WriteString(theme.StyleError.Render("✗ " + s.message))
content.WriteString("\n\n")
content.WriteString(s.table.View())
content.WriteString("\n\n")
content.WriteString(restoreHelpStyle.Render("Actions: [Enter] Restore Selected • [↑/↓] Navigate • [q] Back"))
content.WriteString(theme.StyleHelpKey.Render("Actions: [Enter] Restore Selected • [↑/↓] Navigate • [q] Back"))
return content.String()
}
@@ -224,7 +204,7 @@ func (s *RestoreScreen) renderContent() string {
// Show selected backup details
if len(s.table.Rows()) > 0 && s.selectedBackup != nil {
content.WriteString(restoreInfoStyle.Render(
content.WriteString(theme.StyleMuted.Render(
fmt.Sprintf(
"Selected: %s (%s) - %s\nSize: %s",
s.selectedBackup.Operation,
@@ -236,7 +216,7 @@ func (s *RestoreScreen) renderContent() string {
content.WriteString("\n")
}
content.WriteString(restoreHelpStyle.Render("Actions: [Enter] Restore Selected • [↑/↓] Navigate • [q] Back"))
content.WriteString(theme.StyleHelpKey.Render("Actions: [Enter] Restore Selected • [↑/↓] Navigate • [q] Back"))
return content.String()
}
@@ -284,15 +264,8 @@ func (s *RestoreScreen) buildTable() {
// setTableStyles applies styling to the table
func (s *RestoreScreen) setTableStyles() {
styles := table.DefaultStyles()
styles.Header = styles.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")).
BorderBottom(true).
Bold(true)
styles.Selected = styles.Selected.
Foreground(lipgloss.Color("229")).
Background(lipgloss.Color("57")).
Bold(false)
styles.Header = theme.StyleTableHeader
styles.Selected = theme.StyleTableSelected
s.table.SetStyles(styles)
}