Compare commits
2 Commits
707464e61e
...
8b49fbfd3a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b49fbfd3a | ||
|
|
78a100112c |
File diff suppressed because one or more lines are too long
@@ -26,6 +26,7 @@ type model struct {
|
|||||||
previousScreen screens.Screen
|
previousScreen screens.Screen
|
||||||
quitting bool
|
quitting bool
|
||||||
initialized bool
|
initialized bool
|
||||||
|
errorScreen *screens.ErrorScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) Init() tea.Cmd {
|
func (m model) Init() tea.Cmd {
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ var (
|
|||||||
addHelpStyle = lipgloss.NewStyle().
|
addHelpStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("241")).
|
Foreground(lipgloss.Color("241")).
|
||||||
MarginTop(1)
|
MarginTop(1)
|
||||||
|
loadingStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("62")).
|
||||||
|
Bold(true).
|
||||||
|
MarginTop(1)
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewAddScreen creates a new add screen
|
// NewAddScreen creates a new add screen
|
||||||
@@ -71,14 +75,24 @@ func NewAddScreen() *AddScreen {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Create spinner for loading states
|
||||||
|
s := spinner.New()
|
||||||
|
s.Spinner = spinner.Dot
|
||||||
|
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("62"))
|
||||||
|
|
||||||
return &AddScreen{
|
return &AddScreen{
|
||||||
form: form,
|
form: form,
|
||||||
quitting: false,
|
quitting: false,
|
||||||
|
spinner: s,
|
||||||
|
isCreating: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the add screen
|
// Init initializes the add screen
|
||||||
func (s *AddScreen) Init() tea.Cmd {
|
func (s *AddScreen) Init() tea.Cmd {
|
||||||
|
if s.isCreating {
|
||||||
|
return s.spinner.Tick
|
||||||
|
}
|
||||||
return s.form.Init()
|
return s.form.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,10 +104,19 @@ func (s *AddScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
|
|||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "q", "ctrl+c", "esc":
|
case "q", "ctrl+c", "esc":
|
||||||
// Cancel and return to list
|
// Cancel and return to list (only if not creating)
|
||||||
|
if !s.isCreating {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If creating, update spinner instead of form
|
||||||
|
if s.isCreating {
|
||||||
|
var cmd tea.Cmd
|
||||||
|
s.spinner, cmd = s.spinner.Update(msg)
|
||||||
|
return s, cmd
|
||||||
|
}
|
||||||
|
|
||||||
// Update the form
|
// Update the form
|
||||||
form, cmd := s.form.Update(msg)
|
form, cmd := s.form.Update(msg)
|
||||||
@@ -108,6 +131,8 @@ func (s *AddScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
|
|||||||
dns := s.form.GetString("dns")
|
dns := s.form.GetString("dns")
|
||||||
usePSK := s.form.GetBool("use_psk")
|
usePSK := s.form.GetBool("use_psk")
|
||||||
|
|
||||||
|
// Set creating state and start spinner
|
||||||
|
s.isCreating = true
|
||||||
// Create the client
|
// Create the client
|
||||||
return s, s.createClient(name, dns, usePSK)
|
return s, s.createClient(name, dns, usePSK)
|
||||||
}
|
}
|
||||||
@@ -121,6 +146,16 @@ func (s *AddScreen) View() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.isCreating {
|
||||||
|
return loadingStyle.Render(
|
||||||
|
lipgloss.JoinVertical(
|
||||||
|
lipgloss.Left,
|
||||||
|
addTitleStyle.Render("Add New WireGuard Client"),
|
||||||
|
s.spinner.View()+" Creating client configuration, please wait...",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
content := lipgloss.JoinVertical(
|
content := lipgloss.JoinVertical(
|
||||||
lipgloss.Left,
|
lipgloss.Left,
|
||||||
addTitleStyle.Render("Add New WireGuard Client"),
|
addTitleStyle.Render("Add New WireGuard Client"),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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"
|
||||||
@@ -25,33 +26,11 @@ type DetailScreen struct {
|
|||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
var (
|
var (
|
||||||
detailTitleStyle = lipgloss.NewStyle().
|
detailTitleStyle = lipgloss.NewStyle().Bold(true).MarginTop(0)
|
||||||
Foreground(lipgloss.Color("62")).
|
detailSectionStyle = lipgloss.NewStyle().Bold(true).MarginTop(1)
|
||||||
Bold(true)
|
detailLabelStyle = lipgloss.NewStyle().Width(18)
|
||||||
detailSectionStyle = lipgloss.NewStyle().
|
detailValueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("255"))
|
||||||
Foreground(lipgloss.Color("241")).
|
dimmedContentStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))
|
||||||
Bold(true).
|
|
||||||
MarginTop(1)
|
|
||||||
detailLabelStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("241")).
|
|
||||||
Width(18)
|
|
||||||
detailValueStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("255"))
|
|
||||||
detailConnectedStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("46")).
|
|
||||||
Bold(true)
|
|
||||||
detailDisconnectedStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("196")).
|
|
||||||
Bold(true)
|
|
||||||
detailWarningStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("226")).
|
|
||||||
Bold(true)
|
|
||||||
detailHelpStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("63")).
|
|
||||||
MarginTop(1)
|
|
||||||
detailErrorStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("196")).
|
|
||||||
MarginTop(1)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewDetailScreen creates a new detail screen for a client
|
// NewDetailScreen creates a new detail screen for a client
|
||||||
|
|||||||
242
internal/tui/screens/error.go
Normal file
242
internal/tui/screens/error.go
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
package screens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/calmcacil/wg-admin/internal/tui/theme"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrorScreen displays user-friendly error messages with recovery options
|
||||||
|
type ErrorScreen struct {
|
||||||
|
err error
|
||||||
|
friendly string
|
||||||
|
actions []ErrorAction
|
||||||
|
quitting bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorAction represents a recovery action
|
||||||
|
type ErrorAction struct {
|
||||||
|
Key string
|
||||||
|
Label string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewErrorScreen creates a new error screen with mapped error information
|
||||||
|
func NewErrorScreen(err error) *ErrorScreen {
|
||||||
|
screen := &ErrorScreen{
|
||||||
|
err: err,
|
||||||
|
friendly: err.Error(), // Fallback to raw error
|
||||||
|
actions: []ErrorAction{{Key: "enter", Label: "OK", Description: "Dismiss and return"}},
|
||||||
|
quitting: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map error to user-friendly message and recovery options
|
||||||
|
screen.mapError(err)
|
||||||
|
|
||||||
|
return screen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the error screen
|
||||||
|
func (s *ErrorScreen) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update handles messages for the error screen
|
||||||
|
func (s *ErrorScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "q", "ctrl+c":
|
||||||
|
// Quit application
|
||||||
|
s.quitting = true
|
||||||
|
return s, tea.Quit
|
||||||
|
case "enter", "esc":
|
||||||
|
// Dismiss error screen
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// View renders the error screen
|
||||||
|
func (s *ErrorScreen) View() string {
|
||||||
|
if s.quitting {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
titleStyle := theme.StyleTitle.Copy().MarginBottom(1)
|
||||||
|
errorStyle := theme.StyleError.Copy().Bold(true)
|
||||||
|
msgStyle := theme.StyleSubtitle.Copy().MarginTop(1).MarginBottom(2)
|
||||||
|
actionTitleStyle := theme.StylePrimary.Copy().Bold(true).MarginTop(1)
|
||||||
|
actionStyle := theme.StyleMuted.Copy().MarginLeft(2)
|
||||||
|
|
||||||
|
// Build the error display
|
||||||
|
content := lipgloss.JoinVertical(
|
||||||
|
lipgloss.Left,
|
||||||
|
titleStyle.Render("Error Occurred"),
|
||||||
|
errorStyle.Render("⚠ "+s.friendly),
|
||||||
|
msgStyle.Render("Technical Details: "+s.err.Error()),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add recovery actions
|
||||||
|
if len(s.actions) > 1 {
|
||||||
|
content += "\n" + actionTitleStyle.Render("Recovery Options:")
|
||||||
|
for _, action := range s.actions {
|
||||||
|
key := theme.StyleHelpKey.Render("[" + action.Key + "]")
|
||||||
|
content += "\n" + actionStyle.Render(
|
||||||
|
lipgloss.JoinHorizontal(lipgloss.Left,
|
||||||
|
key+" ",
|
||||||
|
action.Label+" - "+action.Description,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content += "\n\n" + theme.StyleMuted.Render("Press Enter to dismiss • Press q to quit")
|
||||||
|
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Padding(1, 2).
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(theme.GetCurrentTheme().Scheme.Error).
|
||||||
|
Render(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapError converts technical errors to user-friendly messages and recovery options
|
||||||
|
func (s *ErrorScreen) mapError(err error) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errStr := strings.ToLower(err.Error())
|
||||||
|
|
||||||
|
// Permission errors
|
||||||
|
if strings.Contains(errStr, "permission") ||
|
||||||
|
strings.Contains(errStr, "denied") ||
|
||||||
|
strings.Contains(errStr, "operation not permitted") {
|
||||||
|
s.friendly = "Permission Denied"
|
||||||
|
s.actions = []ErrorAction{
|
||||||
|
{Key: "r", Label: "Run with sudo", Description: "Restart with elevated privileges"},
|
||||||
|
{Key: "enter", Label: "Dismiss", Description: "Return to previous screen"},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// File not found errors
|
||||||
|
if strings.Contains(errStr, "no such file") ||
|
||||||
|
strings.Contains(errStr, "file not found") ||
|
||||||
|
strings.Contains(errStr, "does not exist") {
|
||||||
|
s.friendly = "Configuration File Missing"
|
||||||
|
s.actions = []ErrorAction{
|
||||||
|
{Key: "r", Label: "Restore", Description: "Restore from backup (if available)"},
|
||||||
|
{Key: "enter", Label: "Dismiss", Description: "Return to previous screen"},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client already exists errors
|
||||||
|
if strings.Contains(errStr, "already exists") ||
|
||||||
|
strings.Contains(errStr, "duplicate") {
|
||||||
|
s.friendly = "Client Already Exists"
|
||||||
|
s.actions = []ErrorAction{
|
||||||
|
{Key: "n", Label: "New Name", Description: "Try a different client name"},
|
||||||
|
{Key: "enter", Label: "Dismiss", Description: "Return to previous screen"},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IP address exhaustion
|
||||||
|
if strings.Contains(errStr, "no available") ||
|
||||||
|
strings.Contains(errStr, "exhausted") ||
|
||||||
|
strings.Contains(errStr, "out of") {
|
||||||
|
s.friendly = "No Available IP Addresses"
|
||||||
|
s.actions = []ErrorAction{
|
||||||
|
{Key: "d", Label: "Delete Client", Description: "Remove an unused client to free IPs"},
|
||||||
|
{Key: "enter", Label: "Dismiss", Description: "Return to previous screen"},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config file parsing errors
|
||||||
|
if strings.Contains(errStr, "parse") ||
|
||||||
|
strings.Contains(errStr, "invalid") ||
|
||||||
|
strings.Contains(errStr, "malformed") {
|
||||||
|
s.friendly = "Invalid Configuration"
|
||||||
|
s.actions = []ErrorAction{
|
||||||
|
{Key: "r", Label: "Restore", Description: "Restore from backup"},
|
||||||
|
{Key: "m", Label: "Manual Fix", Description: "Edit configuration manually"},
|
||||||
|
{Key: "enter", Label: "Dismiss", Description: "Return to previous screen"},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// WireGuard command failures
|
||||||
|
if strings.Contains(errStr, "wg genkey") ||
|
||||||
|
strings.Contains(errStr, "wg pubkey") ||
|
||||||
|
strings.Contains(errStr, "wg genpsk") ||
|
||||||
|
strings.Contains(errStr, "wg set") {
|
||||||
|
s.friendly = "WireGuard Command Failed"
|
||||||
|
s.actions = []ErrorAction{
|
||||||
|
{Key: "i", Label: "Install WG", Description: "Ensure WireGuard is installed"},
|
||||||
|
{Key: "s", Label: "Check Service", Description: "Verify WireGuard service is running"},
|
||||||
|
{Key: "enter", Label: "Dismiss", Description: "Return to previous screen"},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network errors
|
||||||
|
if strings.Contains(errStr, "network") ||
|
||||||
|
strings.Contains(errStr, "connection") ||
|
||||||
|
strings.Contains(errStr, "timeout") {
|
||||||
|
s.friendly = "Network Error"
|
||||||
|
s.actions = []ErrorAction{
|
||||||
|
{Key: "r", Label: "Retry", Description: "Attempt the operation again"},
|
||||||
|
{Key: "c", Label: "Check Connection", Description: "Verify network connectivity"},
|
||||||
|
{Key: "enter", Label: "Dismiss", Description: "Return to previous screen"},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config directory not found
|
||||||
|
if strings.Contains(errStr, "config directory") ||
|
||||||
|
strings.Contains(errStr, "wireguard") {
|
||||||
|
s.friendly = "WireGuard Not Configured"
|
||||||
|
s.actions = []ErrorAction{
|
||||||
|
{Key: "i", Label: "Install WireGuard", Description: "Set up WireGuard on this server"},
|
||||||
|
{Key: "enter", Label: "Dismiss", Description: "Return to previous screen"},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS validation errors
|
||||||
|
if strings.Contains(errStr, "dns") ||
|
||||||
|
strings.Contains(errStr, "invalid address") {
|
||||||
|
s.friendly = "Invalid DNS Configuration"
|
||||||
|
s.actions = []ErrorAction{
|
||||||
|
{Key: "e", Label: "Edit DNS", Description: "Update DNS server settings"},
|
||||||
|
{Key: "d", Label: "Use Default", Description: "Use default DNS (8.8.8.8, 8.8.4.4)"},
|
||||||
|
{Key: "enter", Label: "Dismiss", Description: "Return to previous screen"},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup/restore errors
|
||||||
|
if strings.Contains(errStr, "backup") ||
|
||||||
|
strings.Contains(errStr, "restore") {
|
||||||
|
s.friendly = "Backup Operation Failed"
|
||||||
|
s.actions = []ErrorAction{
|
||||||
|
{Key: "r", Label: "Retry", Description: "Attempt backup/restore again"},
|
||||||
|
{Key: "c", Label: "Check Space", Description: "Verify disk space is available"},
|
||||||
|
{Key: "enter", Label: "Dismiss", Description: "Return to previous screen"},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: generic error
|
||||||
|
s.friendly = "An Error Occurred"
|
||||||
|
s.actions = []ErrorAction{
|
||||||
|
{Key: "r", Label: "Retry", Description: "Attempt the operation again"},
|
||||||
|
{Key: "enter", Label: "Dismiss", Description: "Return to previous screen"},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package screens
|
package screens
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -135,17 +136,62 @@ func (s *ListScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// View renders the list screen
|
// View renders the list screen
|
||||||
|
// Breadcrumb: Home
|
||||||
|
breadcrumb := components.RenderBreadcrumb([]components.BreadcrumbItem{
|
||||||
|
{Label: "Clients", ID: "list"},
|
||||||
|
})
|
||||||
|
|
||||||
func (s *ListScreen) View() string {
|
func (s *ListScreen) View() string {
|
||||||
if len(s.clients) == 0 {
|
if len(s.clients) == 0 {
|
||||||
return s.search.View() + "\n" + "No clients found. Press 'r' to refresh or 'q' to quit."
|
// Empty state with helpful guidance
|
||||||
|
return s.search.View() + "\n\n" +
|
||||||
|
lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("226")).
|
||||||
|
Bold(true).
|
||||||
|
Render("No clients yet") + "\n" +
|
||||||
|
lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("241")).
|
||||||
|
Render("Let's get started! Here are your options:") + "\n" +
|
||||||
|
lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("109")).
|
||||||
|
Render(" • Press [a] to add your first client") + "\n" +
|
||||||
|
lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("109")).
|
||||||
|
Render(" • Press [R] to restore from backup") + "\n" +
|
||||||
|
lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("109")).
|
||||||
|
Render(" • Press [r] to refresh the client list") + "\n" +
|
||||||
|
lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("109")).
|
||||||
|
Render(" • Press [q] to quit")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there are no matches
|
// Check if there are no matches
|
||||||
if s.search.IsActive() && len(s.filtered) == 0 && s.search.GetQuery() != "" {
|
if s.search.IsActive() && len(s.filtered) == 0 && s.search.GetQuery() != "" {
|
||||||
return s.search.View() + "\n" + lipgloss.NewStyle().
|
// Empty search results with helpful tips
|
||||||
|
return s.search.View() + "\n\n" +
|
||||||
|
lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("226")).
|
||||||
|
Bold(true).
|
||||||
|
Render("No matching clients found") + "\n" +
|
||||||
|
lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("241")).
|
Foreground(lipgloss.Color("241")).
|
||||||
Italic(true).
|
Render("Search tips:") + "\n" +
|
||||||
Render("No matching clients found. Try a different search term.")
|
lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("109")).
|
||||||
|
Render(" • Check your spelling") + "\n" +
|
||||||
|
lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("109")).
|
||||||
|
Render(" • Try a shorter search term") + "\n" +
|
||||||
|
lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("109")).
|
||||||
|
Render(" • Search by name, IP, or status") + "\n" +
|
||||||
|
lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("109")).
|
||||||
|
Render(" • Press [esc] to clear search") + "\n" +
|
||||||
|
lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("109")).
|
||||||
|
Render(" • Press [r] to refresh client list")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate time since last update
|
// Calculate time since last update
|
||||||
@@ -228,13 +274,21 @@ func (s *ListScreen) applyFilter() {
|
|||||||
s.buildTable()
|
s.buildTable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatStatusWithIcon formats the status with a colored circle icon
|
||||||
|
func (s *ListScreen) formatStatusWithIcon(status string) string {
|
||||||
|
if status == wireguard.StatusConnected {
|
||||||
|
return lipgloss.NewStyle().Foreground(lipgloss.Color("46")).Render("●") + " " + status
|
||||||
|
}
|
||||||
|
return lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Render("●") + " " + status
|
||||||
|
}
|
||||||
|
|
||||||
// buildTable creates and configures the table
|
// buildTable creates and configures the table
|
||||||
func (s *ListScreen) buildTable() {
|
func (s *ListScreen) buildTable() {
|
||||||
columns := []table.Column{
|
columns := []table.Column{
|
||||||
{Title: "Name", Width: 20},
|
{Title: "Name", Width: 20},
|
||||||
{Title: "IPv4", Width: 15},
|
{Title: "IPv4", Width: 15},
|
||||||
{Title: "IPv6", Width: 35},
|
{Title: "IPv6", Width: 35},
|
||||||
{Title: "Status", Width: 12},
|
{Title: "Status", Width: 14},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use filtered clients if search is active, otherwise use all clients
|
// Use filtered clients if search is active, otherwise use all clients
|
||||||
@@ -245,11 +299,12 @@ 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)
|
||||||
row := table.Row{
|
row := table.Row{
|
||||||
cws.Client.Name,
|
cws.Client.Name,
|
||||||
cws.Client.IPv4,
|
cws.Client.IPv4,
|
||||||
cws.Client.IPv6,
|
cws.Client.IPv6,
|
||||||
cws.Status,
|
statusText,
|
||||||
}
|
}
|
||||||
rows = append(rows, row)
|
rows = append(rows, row)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/charmbracelet/bubbles/spinner"
|
||||||
"github.com/charmbracelet/bubbles/table"
|
"github.com/charmbracelet/bubbles/table"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
@@ -22,6 +23,7 @@ type RestoreScreen struct {
|
|||||||
restoreError error
|
restoreError error
|
||||||
restoreSuccess bool
|
restoreSuccess bool
|
||||||
message string
|
message string
|
||||||
|
spinner spinner.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
@@ -41,6 +43,9 @@ var (
|
|||||||
restoreInfoStyle = lipgloss.NewStyle().
|
restoreInfoStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("241")).
|
Foreground(lipgloss.Color("241")).
|
||||||
MarginTop(1)
|
MarginTop(1)
|
||||||
|
loadingStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("62")).
|
||||||
|
Bold(true)
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewRestoreScreen creates a new restore screen
|
// NewRestoreScreen creates a new restore screen
|
||||||
|
|||||||
Reference in New Issue
Block a user