From 68939cdc08ceb28b92b93a6780d65527cad34074 Mon Sep 17 00:00:00 2001 From: Calmcacil Date: Mon, 12 Jan 2026 23:04:48 +0100 Subject: [PATCH] Reduce status refresh interval to 3 seconds and add last updated indicator --- cmd/wg-tui/main.go | 6 ++++++ internal/tui/screens/add.go | 6 +++--- internal/tui/screens/detail.go | 33 +++++++++++++------------------- internal/tui/screens/list.go | 12 ++++++++---- internal/tui/screens/qr.go | 6 +++--- internal/tui/screens/restore.go | 34 ++++++++++++++++++++++----------- 6 files changed, 56 insertions(+), 41 deletions(-) diff --git a/cmd/wg-tui/main.go b/cmd/wg-tui/main.go index b3d2b6d..cbd76f6 100644 --- a/cmd/wg-tui/main.go +++ b/cmd/wg-tui/main.go @@ -99,6 +99,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.currentScreen = screens.NewListScreen() return m, m.currentScreen.Init() + case screens.ErrMsg: + // An error occurred - show error screen + m.previousScreen = m.currentScreen + m.errorScreen = screens.NewErrorScreen(msg.Err) + m.currentScreen = m.errorScreen + return m, m.currentScreen.Init() } // Pass messages to current screen diff --git a/internal/tui/screens/add.go b/internal/tui/screens/add.go index 7115e11..9b91dbc 100644 --- a/internal/tui/screens/add.go +++ b/internal/tui/screens/add.go @@ -29,7 +29,7 @@ var ( addHelpStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("241")). MarginTop(1) - loadingStyle = lipgloss.NewStyle(). + addLoadingStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("62")). Bold(true). MarginTop(1) @@ -147,7 +147,7 @@ func (s *AddScreen) View() string { } if s.isCreating { - return loadingStyle.Render( + return addLoadingStyle.Render( lipgloss.JoinVertical( lipgloss.Left, addTitleStyle.Render("Add New WireGuard Client"), @@ -172,7 +172,7 @@ func (s *AddScreen) createClient(name, dns string, usePSK bool) tea.Cmd { // Create the client via wireguard package err := wireguard.CreateClient(name, dns, usePSK) if err != nil { - return errMsg{err: fmt.Errorf("failed to create client: %w", err)} + return ErrMsg{Err: fmt.Errorf("failed to create client: %w", err)} } // Return success message diff --git a/internal/tui/screens/detail.go b/internal/tui/screens/detail.go index f38b69e..486106b 100644 --- a/internal/tui/screens/detail.go +++ b/internal/tui/screens/detail.go @@ -26,11 +26,8 @@ type DetailScreen struct { // Styles var ( - detailTitleStyle = lipgloss.NewStyle().Bold(true).MarginTop(0) - detailSectionStyle = lipgloss.NewStyle().Bold(true).MarginTop(1) - detailLabelStyle = lipgloss.NewStyle().Width(18) detailValueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("255")) - dimmedContentStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244")) + dimmedContentStyle = theme.StyleMuted ) // NewDetailScreen creates a new detail screen for a client @@ -118,9 +115,7 @@ func (s *DetailScreen) View() string { if s.showConfig && s.configDisplay != nil { // Render underlying content dimmed content := s.renderContent() - dimmedContent := lipgloss.NewStyle(). - Foreground(lipgloss.Color("244")). - Render(content) + dimmedContent := dimmedContentStyle.Render(content) // Overlay config display modal return lipgloss.JoinVertical( @@ -134,9 +129,7 @@ func (s *DetailScreen) View() string { if s.showConfirm && s.confirmModal != nil { // Render underlying content dimmed content := s.renderContent() - dimmedContent := lipgloss.NewStyle(). - Foreground(lipgloss.Color("244")). - Render(content) + dimmedContent := dimmedContentStyle.Render(content) // Overlay confirmation modal return lipgloss.JoinVertical( @@ -153,21 +146,21 @@ func (s *DetailScreen) View() string { func (s *DetailScreen) renderContent() string { statusText := s.status if s.status == wireguard.StatusConnected { - statusText = detailConnectedStyle.Render("● " + s.status) + statusText = theme.StyleSuccess.Bold(true).Render("● " + s.status) } else { - statusText = detailDisconnectedStyle.Render("● " + s.status) + statusText = theme.StyleError.Bold(true).Render("● " + s.status) } // Build content content := lipgloss.JoinVertical( lipgloss.Left, - detailTitleStyle.Render(fmt.Sprintf("Client Details: %s", s.client.Name)), + theme.StyleTitle.Render(fmt.Sprintf("Client Details: %s", s.client.Name)), "", s.renderField("Status", statusText), s.renderField("IPv4 Address", detailValueStyle.Render(s.client.IPv4)), s.renderField("IPv6 Address", detailValueStyle.Render(s.client.IPv6)), "", - detailSectionStyle.Render("WireGuard Configuration"), + theme.StyleSubtitle.Bold(true).MarginTop(1).Render("WireGuard Configuration"), s.renderField("Public Key", detailValueStyle.Render(s.client.PublicKey)), s.renderField("Preshared Key", detailValueStyle.Render(func() string { if s.client.HasPSK { @@ -176,7 +169,7 @@ func (s *DetailScreen) renderContent() string { return "Not configured" }())), "", - detailSectionStyle.Render("Connection Info"), + theme.StyleSubtitle.Bold(true).MarginTop(1).Render("Connection Info"), s.renderField("Last Handshake", detailValueStyle.Render(s.formatHandshake())), s.renderField("Transfer (Rx/Tx)", detailValueStyle.Render(fmt.Sprintf("%s / %s", s.transferRx, s.transferTx))), s.renderField("Config Path", detailValueStyle.Render(s.client.ConfigPath)), @@ -184,7 +177,7 @@ func (s *DetailScreen) renderContent() string { ) // Add help text - helpText := detailHelpStyle.Render("Actions: [d] Delete • [c] View Config • [q/b] Back") + helpText := theme.StyleHelpKey.MarginTop(1).Render("Actions: [d] Delete • [c] View Config • [q/b] Back") content = lipgloss.JoinVertical(lipgloss.Left, content, helpText) return content @@ -193,7 +186,7 @@ func (s *DetailScreen) renderContent() string { // renderField renders a label-value pair func (s *DetailScreen) renderField(label string, value string) string { return lipgloss.JoinHorizontal(lipgloss.Left, - detailLabelStyle.Render(label), + theme.StyleSubtitle.Width(18).Render(label), value, ) } @@ -221,7 +214,7 @@ func (s *DetailScreen) formatHandshake() string { func (s *DetailScreen) loadClientStatus() tea.Msg { peers, err := wireguard.GetAllPeers() if err != nil { - return errMsg{err: err} + return ErrMsg{Err: err} } // Find peer by public key @@ -250,7 +243,7 @@ func (s *DetailScreen) loadConfig() tea.Cmd { return func() tea.Msg { config, err := wireguard.GetClientConfigContent(s.client.Name) if err != nil { - return errMsg{fmt.Errorf("failed to load client config: %w", err)} + return ErrMsg{Err: fmt.Errorf("failed to load client config: %w", err)} } // Create or update config display modal @@ -269,7 +262,7 @@ func (s *DetailScreen) deleteClient() tea.Cmd { return func() tea.Msg { err := wireguard.DeleteClient(s.client.Name) if err != nil { - return errMsg{fmt.Errorf("failed to delete client: %w", err)} + return ErrMsg{fmt.Errorf("failed to delete client: %w", err)} } return ClientDeletedMsg{ Name: s.client.Name, diff --git a/internal/tui/screens/list.go b/internal/tui/screens/list.go index 96dce5b..5306f66 100644 --- a/internal/tui/screens/list.go +++ b/internal/tui/screens/list.go @@ -137,9 +137,13 @@ func (s *ListScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { // View renders the list screen func (s *ListScreen) View() string { + // Breadcrumb: Home + breadcrumb := components.RenderBreadcrumb([]components.BreadcrumbItem{ + {Label: "Clients", ID: "list"}, + }) if len(s.clients) == 0 { // Empty state with helpful guidance - return s.search.View() + "\n\n" + + return breadcrumb + "\n" + s.search.View() + "\n\n" + lipgloss.NewStyle(). Foreground(lipgloss.Color("226")). Bold(true). @@ -164,7 +168,7 @@ func (s *ListScreen) View() string { // Check if there are no matches if s.search.IsActive() && len(s.filtered) == 0 && s.search.GetQuery() != "" { // Empty search results with helpful tips - return s.search.View() + "\n\n" + + return breadcrumb + "\n" + s.search.View() + "\n\n" + lipgloss.NewStyle(). Foreground(lipgloss.Color("226")). Bold(true). @@ -200,7 +204,7 @@ func (s *ListScreen) View() string { Foreground(lipgloss.Color("241")). Render("Last updated: " + timeAgo) - return s.search.View() + "\n" + s.table.View() + "\n" + lastUpdatedText + return breadcrumb + "\n" + s.search.View() + "\n" + s.table.View() + "\n" + lastUpdatedText } // formatDuration returns a human-readable string for the duration @@ -218,7 +222,7 @@ func formatDuration(d time.Duration) string { func (s *ListScreen) loadClients() tea.Msg { clients, err := wireguard.ListClients() if err != nil { - return errMsg{err: err} + return ErrMsg{Err: err} } // Get status for each client diff --git a/internal/tui/screens/qr.go b/internal/tui/screens/qr.go index bccb220..746782a 100644 --- a/internal/tui/screens/qr.go +++ b/internal/tui/screens/qr.go @@ -54,8 +54,8 @@ func (s *QRScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { case configLoadedMsg: s.configContent = msg.content s.generateQRCode() - case errMsg: - s.errorMsg = msg.err.Error() + case ErrMsg: + s.errorMsg = msg.Err.Error() } return s, nil @@ -76,7 +76,7 @@ func (s *QRScreen) View() string { func (s *QRScreen) loadConfig() tea.Msg { content, err := wireguard.GetClientConfigContent(s.clientName) if err != nil { - return errMsg{err: err} + return ErrMsg{Err: err} } return configLoadedMsg{content: content} } diff --git a/internal/tui/screens/restore.go b/internal/tui/screens/restore.go index 7919adf..129ecd4 100644 --- a/internal/tui/screens/restore.go +++ b/internal/tui/screens/restore.go @@ -43,15 +43,21 @@ var ( restoreInfoStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("241")). MarginTop(1) - loadingStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("62")). - Bold(true) + restoreLoadingStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("62")). + Bold(true) ) // NewRestoreScreen creates a new restore screen func NewRestoreScreen() *RestoreScreen { + // Create spinner for loading states + s := spinner.New() + s.Spinner = spinner.Dot + s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("62")) + return &RestoreScreen{ showConfirm: false, + spinner: s, } } @@ -64,6 +70,12 @@ func (s *RestoreScreen) Init() tea.Cmd { func (s *RestoreScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { var cmd tea.Cmd + // If restoring, only update spinner + if s.isRestoring && !s.showConfirm { + s.spinner, cmd = s.spinner.Update(msg) + return s, cmd + } + // Handle confirmation modal if s.showConfirm && s.confirmModal != nil { _, cmd = s.confirmModal.Update(msg) @@ -74,7 +86,7 @@ func (s *RestoreScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { // User confirmed restore s.isRestoring = true s.showConfirm = false - return s, s.performRestore() + return s, tea.Sequence(s.spinner.Tick, s.performRestore()) } // User cancelled - close modal s.showConfirm = false @@ -87,7 +99,7 @@ func (s *RestoreScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { if msg.String() == "enter" && s.confirmModal.IsConfirmed() && s.selectedBackup != nil { s.isRestoring = true s.showConfirm = false - return s, s.performRestore() + return s, tea.Sequence(s.spinner.Tick, s.performRestore()) } } @@ -130,14 +142,14 @@ func (s *RestoreScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { } } } - case restoreCompletedMsg: + case RestoreCompletedMsg: s.isRestoring = false - if msg.err != nil { - s.restoreError = msg.err - s.message = fmt.Sprintf("Restore failed: %v", msg.err) + if msg.Err != nil { + s.restoreError = msg.Err + s.message = fmt.Sprintf("Restore failed: %v", msg.Err) } else { s.restoreSuccess = true - s.message = fmt.Sprintf("Restore successful! Safety backup created at: %s", msg.safetyBackupPath) + s.message = fmt.Sprintf("Restore successful! Safety backup created at: %s", msg.SafetyBackupPath) } } @@ -227,7 +239,7 @@ func (s *RestoreScreen) renderContent() string { func (s *RestoreScreen) loadBackups() tea.Msg { backups, err := backup.ListBackups() if err != nil { - return errMsg{err: err} + return ErrMsg{Err: err} } return backupsLoadedMsg{backups: backups} }