package screens import ( "fmt" "time" "github.com/calmcacil/wg-admin/internal/tui/components" "github.com/calmcacil/wg-admin/internal/wireguard" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // DetailScreen displays detailed information about a single WireGuard client type DetailScreen struct { client wireguard.Client status string lastHandshake time.Time transferRx string transferTx string confirmModal *components.DeleteConfirmModel showConfirm bool configDisplay *components.ConfigDisplayModel showConfig bool } // Styles var ( detailTitleStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("62")). Bold(true) detailSectionStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("241")). 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 func NewDetailScreen(client wireguard.Client) *DetailScreen { return &DetailScreen{ client: client, showConfirm: false, } } // Init initializes the detail screen func (s *DetailScreen) Init() tea.Cmd { return s.loadClientStatus } // Update handles messages for the detail screen func (s *DetailScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { var cmd tea.Cmd // Handle confirmation modal if s.showConfirm && s.confirmModal != nil { _, cmd = s.confirmModal.Update(msg) // Handle confirmation result if !s.confirmModal.Visible { if s.confirmModal.IsConfirmed() { // User confirmed deletion return s, tea.Batch(s.deleteClient(), func() tea.Msg { return CloseDetailScreenMsg{} }) } // User cancelled - close modal s.showConfirm = false return s, nil } return s, cmd } // Handle config display modal if s.showConfig && s.configDisplay != nil { _, cmd = s.configDisplay.Update(msg) // Handle modal close if !s.configDisplay.IsVisible() { s.showConfig = false return s, nil } return s, cmd } // Handle normal screen messages switch msg := msg.(type) { case clientStatusLoadedMsg: s.status = msg.status s.lastHandshake = msg.lastHandshake s.transferRx = msg.transferRx s.transferTx = msg.transferTx case tea.KeyMsg: switch msg.String() { case "b", "esc": // Return to list screen - signal parent to switch screens return nil, nil case "d": // Show delete confirmation s.confirmModal = components.NewDeleteConfirm( s.client.Name, 80, 24, ) s.showConfirm = true case "c": // Show client configuration return s, s.loadConfig() } } return s, cmd } // View renders the detail screen func (s *DetailScreen) View() string { // Handle config display modal if s.showConfig && s.configDisplay != nil { return s.configDisplay.View() } // Handle confirmation modal if s.showConfirm && s.confirmModal != nil { // Render underlying content dimmed content := s.renderContent() dimmedContent := lipgloss.NewStyle(). Foreground(lipgloss.Color("244")). Render(content) // Overlay confirmation modal return lipgloss.JoinVertical( lipgloss.Left, dimmedContent, s.confirmModal.View(), ) } return s.renderContent() } // renderContent renders the main detail screen content func (s *DetailScreen) renderContent() string { statusText := s.status if s.status == wireguard.StatusConnected { statusText = detailConnectedStyle.Render(s.status) } else { statusText = detailDisconnectedStyle.Render(s.status) } // Build content content := lipgloss.JoinVertical( lipgloss.Left, detailTitleStyle.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"), s.renderField("Public Key", detailValueStyle.Render(s.client.PublicKey)), s.renderField("Preshared Key", detailValueStyle.Render(func() string { if s.client.HasPSK { return "✓ Configured" } return "Not configured" }())), "", detailSectionStyle.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)), "", ) // Add help text helpText := detailHelpStyle.Render("Actions: [d] Delete • [c] View Config • [b] Back") content = lipgloss.JoinVertical(lipgloss.Left, content, helpText) return content } // renderField renders a label-value pair func (s *DetailScreen) renderField(label string, value string) string { return lipgloss.JoinHorizontal(lipgloss.Left, detailLabelStyle.Render(label), value, ) } // formatHandshake formats the last handshake time func (s *DetailScreen) formatHandshake() string { if s.lastHandshake.IsZero() { return "Never" } duration := time.Since(s.lastHandshake) if duration < time.Minute { return "Just now" } else if duration < time.Hour { return fmt.Sprintf("%d min ago", int(duration.Minutes())) } else if duration < 24*time.Hour { return fmt.Sprintf("%d hours ago", int(duration.Hours())) } else if duration < 7*24*time.Hour { return fmt.Sprintf("%d days ago", int(duration.Hours()/24)) } return s.lastHandshake.Format("2006-01-02 15:04") } // loadClientStatus loads the current status of the client func (s *DetailScreen) loadClientStatus() tea.Msg { peers, err := wireguard.GetAllPeers() if err != nil { return errMsg{err: err} } // Find peer by public key for _, peer := range peers { if peer.PublicKey == s.client.PublicKey { return clientStatusLoadedMsg{ status: peer.Status, lastHandshake: peer.LatestHandshake, transferRx: peer.TransferRx, transferTx: peer.TransferTx, } } } // Peer not found in active list return clientStatusLoadedMsg{ status: wireguard.StatusDisconnected, lastHandshake: time.Time{}, transferRx: "", transferTx: "", } } // loadConfig loads and displays the client configuration 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)} } // Create or update config display modal if s.configDisplay == nil { s.configDisplay = components.NewConfigDisplay(config, 80, 24) } else { s.configDisplay.Show(config) } s.showConfig = true return nil } } // deleteClient deletes the client 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 ClientDeletedMsg{ Name: s.client.Name, } } } // Messages // clientStatusLoadedMsg is sent when client status is loaded type clientStatusLoadedMsg struct { status string lastHandshake time.Time transferRx string transferTx string }