Compare commits

...

6 Commits

Author SHA1 Message Date
Calmcacil
9c0da13a19 bd sync: 2026-01-12 23:44:57 2026-01-12 23:44:57 +01:00
Calmcacil
99b9dc17db Enhance search with match highlighting, count display, Ctrl+U to clear 2026-01-12 23:43:24 +01:00
Calmcacil
6629598574 bd sync: 2026-01-12 23:42:48 2026-01-12 23:42:48 +01:00
Calmcacil
50321a8471 Add connection quality indicators based on handshake time 2026-01-12 23:42:38 +01:00
Calmcacil
bf71a7a659 bd sync: 2026-01-12 23:41:27 2026-01-12 23:41:27 +01:00
Calmcacil
f154c7ff69 Standardize TUI formatting and styling across all screens 2026-01-12 23:41:07 +01:00
9 changed files with 163 additions and 151 deletions

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,7 @@ package components
import ( import (
"fmt" "fmt"
"github.com/calmcacil/wg-admin/internal/tui/theme"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
@@ -18,37 +19,12 @@ type DeleteConfirmModel struct {
showErrorMessage bool showErrorMessage bool
} }
// Styles // Local styles for modal
var ( var (
deleteModalStyle = lipgloss.NewStyle(). modalBaseStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("196")).Background(lipgloss.Color("235"))
Border(lipgloss.RoundedBorder()). modalTitleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("226")).Bold(true)
BorderForeground(lipgloss.Color("196")). modalMessageStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("255"))
Padding(1, 3). modalHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
Background(lipgloss.Color("235"))
deleteTitleStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("226")).
Bold(true)
deleteMessageStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("255")).
Width(60)
deleteWarningStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("196")).
Bold(true).
MarginTop(1)
deleteInputStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("255")).
Width(60)
deleteHelpStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("241")).
MarginTop(1)
deleteErrorStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("196")).
Bold(true).
MarginTop(1)
deleteSuccessStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("46")).
Bold(true).
MarginTop(1)
) )
// NewDeleteConfirm creates a new deletion confirmation modal // NewDeleteConfirm creates a new deletion confirmation modal
@@ -108,12 +84,12 @@ func (m *DeleteConfirmModel) View() string {
matches := m.input.Value() == m.clientName matches := m.input.Value() == m.clientName
// Build warning section // Build warning section
warningText := deleteWarningStyle.Render( warningText := theme.StyleError.Bold(true).MarginTop(1).Render(
fmt.Sprintf("⚠️ This will permanently delete client '%s'", m.clientName), fmt.Sprintf("⚠️ This will permanently delete client '%s'", m.clientName),
) )
// Build message section // Build message section
messageText := deleteMessageStyle.Render( messageText := modalMessageStyle.Width(60).Render(
fmt.Sprintf("This action cannot be undone. Please type the client name to confirm deletion."), fmt.Sprintf("This action cannot be undone. Please type the client name to confirm deletion."),
) )
@@ -121,29 +97,29 @@ func (m *DeleteConfirmModel) View() string {
inputSection := lipgloss.JoinVertical( inputSection := lipgloss.JoinVertical(
lipgloss.Left, lipgloss.Left,
"", "",
deleteInputStyle.Render("Client name:"), theme.StyleValue.Width(60).Render("Client name:"),
m.input.View(), m.input.View(),
) )
// Build status section // Build status section
var statusText string var statusText string
if matches { if matches {
statusText = deleteSuccessStyle.Render("✓ Client name matches. Press Enter to confirm deletion.") statusText = theme.StyleSuccess.Bold(true).MarginTop(1).Render("✓ Client name matches. Press Enter to confirm deletion.")
} else if m.showErrorMessage { } else if m.showErrorMessage {
statusText = deleteErrorStyle.Render("✗ Client name does not match. Please try again.") statusText = theme.StyleError.Bold(true).MarginTop(1).Render("✗ Client name does not match. Please try again.")
} else if m.input.Value() != "" { } else if m.input.Value() != "" {
statusText = deleteHelpStyle.Render("Client name does not match yet...") statusText = modalHelpStyle.Render("Client name does not match yet...")
} else { } else {
statusText = deleteHelpStyle.Render("Type the client name to enable confirmation.") statusText = modalHelpStyle.Render("Type the client name to enable confirmation.")
} }
// Build help section // Build help section
helpText := deleteHelpStyle.Render("Esc to cancel • Enter to confirm (when name matches)") helpText := modalHelpStyle.Render("Esc to cancel • Enter to confirm (when name matches)")
// Build modal content // Build modal content
content := lipgloss.JoinVertical( content := lipgloss.JoinVertical(
lipgloss.Left, lipgloss.Left,
deleteTitleStyle.Render("🗑️ Delete Client"), modalTitleStyle.Render("🗑️ Delete Client"),
"", "",
warningText, warningText,
"", "",
@@ -155,7 +131,7 @@ func (m *DeleteConfirmModel) View() string {
) )
// Apply modal style // Apply modal style
modal := deleteModalStyle.Render(content) modal := modalBaseStyle.Padding(1, 3).Render(content)
// Center modal on screen // Center modal on screen
modalWidth := lipgloss.Width(modal) modalWidth := lipgloss.Width(modal)
@@ -174,7 +150,7 @@ func (m *DeleteConfirmModel) View() string {
lipgloss.Left, lipgloss.Top, lipgloss.Left, lipgloss.Top,
modal, modal,
lipgloss.WithWhitespaceChars(" "), lipgloss.WithWhitespaceChars(" "),
lipgloss.WithWhitespaceForeground(lipgloss.Color("235")), lipgloss.WithWhitespaceForeground(theme.StyleBackground),
) )
} }

View File

@@ -1,7 +1,9 @@
package components package components
import ( import (
"strconv" "fmt"
"github.com/calmcacil/wg-admin/internal/tui/theme"
"strings" "strings"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
@@ -30,13 +32,15 @@ type SearchModel struct {
} }
// Styles // Styles
// Styles (using theme package)
var ( var (
searchBarStyle = lipgloss.NewStyle(). searchBarStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("255")). Foreground(lipgloss.Color("255")).
Background(lipgloss.Color("235")). Background(theme.StyleBackground).
Padding(0, 1). Padding(0, 1).
Border(lipgloss.RoundedBorder()). Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("240")) BorderForeground(theme.StyleBorder)
searchPromptStyle = lipgloss.NewStyle(). searchPromptStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("226")). Foreground(lipgloss.Color("226")).
Bold(true) Bold(true)
@@ -88,6 +92,10 @@ func (m *SearchModel) Update(msg tea.Msg) (*SearchModel, tea.Cmd) {
m.input.Reset() m.input.Reset()
m.matchCount = m.totalCount m.matchCount = m.totalCount
return m, nil return m, nil
case "ctrl+u":
m.input.Reset()
m.matchCount = m.totalCount
return m, nil
case "tab": case "tab":
m.cycleFilterType() m.cycleFilterType()
return m, nil return m, nil
@@ -141,7 +149,7 @@ func (m *SearchModel) View() string {
helpText := "" helpText := ""
if m.active { if m.active {
helpText = searchHelpStyle.Render(" | Tab: filter | Esc: clear") helpText = searchHelpStyle.Render(" | Tab: filter | Ctrl+U: clear | Esc: exit")
} else { } else {
helpText = searchHelpStyle.Render(" | /: search") helpText = searchHelpStyle.Render(" | /: search")
} }
@@ -270,7 +278,7 @@ func (m *SearchModel) HighlightMatches(value string) string {
return lipgloss.JoinHorizontal( return lipgloss.JoinHorizontal(
lipgloss.Left, lipgloss.Left,
before, before,
matchStyle.Render(string(match)), matchStyle.Render(match),
after, after,
) )
} }
@@ -296,7 +304,7 @@ func (m *SearchModel) renderCount(count int) string {
Foreground(lipgloss.Color("196")). Foreground(lipgloss.Color("196")).
Render("No matches") Render("No matches")
} }
return searchCountStyle.Render(string(rune('0' + count))) return searchCountStyle.Render(fmt.Sprintf("%d", count))
} }
// ClientData represents client data for filtering // ClientData represents client data for filtering

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/calmcacil/wg-admin/internal/tui/components" "github.com/calmcacil/wg-admin/internal/tui/components"
"github.com/calmcacil/wg-admin/internal/tui/theme"
"github.com/calmcacil/wg-admin/internal/wireguard" "github.com/calmcacil/wg-admin/internal/wireguard"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
@@ -102,39 +103,22 @@ func (s *QRScreen) generateQRCode() {
// renderQR renders the QR code with styling // renderQR renders the QR code with styling
func (s *QRScreen) renderQR() string { 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(). styleQR := lipgloss.NewStyle().
MarginLeft(2) 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" 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 // renderError renders an error message
func (s *QRScreen) renderError() string { func (s *QRScreen) renderError() string {
styleError := lipgloss.NewStyle(). title := theme.StyleError.Bold(true).Render("Error")
Foreground(lipgloss.Color("196")).
Bold(true)
styleHelp := lipgloss.NewStyle().
Foreground(lipgloss.Color("241")).
MarginTop(1)
title := styleError.Render("Error")
message := s.errorMsg message := s.errorMsg
help := "Press [q/Escape] to return" 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 // Messages

View File

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

View File

@@ -211,6 +211,54 @@ func ApplyTheme(theme *Theme) {
StyleBackground = lipgloss.Color("235") StyleBackground = lipgloss.Color("235")
} }
// Modal styles
var (
// ModalBaseStyle is the base style for all modals
ModalBaseStyle = func() lipgloss.Style {
return lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("196")).
Padding(1, 2).
Background(StyleBackground)
}
// ModalTitleStyle is the style for modal titles
ModalTitleStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("226")).
Bold(true)
// ModalMessageStyle is the style for modal messages
ModalMessageStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("255")).
Width(50)
// ModalHelpStyle is the style for modal help text
ModalHelpStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("241")).
MarginTop(1)
// ModalSelectedStyle is the style for selected modal options
ModalSelectedStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("57")).
Bold(true).
Underline(true)
// ModalUnselectedStyle is the style for unselected modal options
ModalUnselectedStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("241"))
)
// Status icon styles
var (
// StatusConnectedStyle is the style for connected status icons
StatusConnectedStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("46"))
// StatusDisconnectedStyle is the style for disconnected status icons
StatusDisconnectedStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("196"))
)
// GetThemeNames returns a list of available theme names // GetThemeNames returns a list of available theme names
func GetThemeNames() []string { func GetThemeNames() []string {
names := make([]string, 0, len(ThemeRegistry)) names := make([]string, 0, len(ThemeRegistry))

View File

@@ -173,7 +173,7 @@ func finalizePeerStatus(peer *PeerStatus, handshake string, transfer string) Pee
// This allows for ~12 missed keepalive intervals (at 25 seconds each) // This allows for ~12 missed keepalive intervals (at 25 seconds each)
if timeSinceHandshake < 5*time.Minute { if timeSinceHandshake < 5*time.Minute {
peer.Status = StatusConnected peer.Status = StatusConnected
peer.Quality = calculateQuality(timeSinceHandshake) peer.Quality = CalculateQuality(timeSinceHandshake)
} else { } else {
peer.Status = StatusDisconnected peer.Status = StatusDisconnected
} }