Fix help screen documentation - incorrect key binding for viewing details
This commit is contained in:
File diff suppressed because one or more lines are too long
54
internal/tui/components/breadcrumb.go
Normal file
54
internal/tui/components/breadcrumb.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BreadcrumbItem represents a single item in the breadcrumb trail
|
||||||
|
type BreadcrumbItem struct {
|
||||||
|
Label string
|
||||||
|
ID string // Optional identifier for the screen
|
||||||
|
}
|
||||||
|
|
||||||
|
// BreadcrumbStyle defines the appearance of breadcrumbs
|
||||||
|
var (
|
||||||
|
breadcrumbStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("241")).
|
||||||
|
MarginBottom(1)
|
||||||
|
breadcrumbSeparatorStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("240"))
|
||||||
|
breadcrumbItemStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("241"))
|
||||||
|
breadcrumbCurrentStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("62")).
|
||||||
|
Bold(true)
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenderBreadcrumb renders a breadcrumb trail from a slice of items
|
||||||
|
func RenderBreadcrumb(items []BreadcrumbItem) string {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
for i, item := range items {
|
||||||
|
var text string
|
||||||
|
if i == len(items)-1 {
|
||||||
|
// Last item - current page
|
||||||
|
text = breadcrumbCurrentStyle.Render(item.Label)
|
||||||
|
} else {
|
||||||
|
// Non-last items - clickable/previous pages
|
||||||
|
text = breadcrumbItemStyle.Render(item.Label)
|
||||||
|
}
|
||||||
|
parts = append(parts, text)
|
||||||
|
|
||||||
|
// Add separator if not last item
|
||||||
|
if i < len(items)-1 {
|
||||||
|
parts = append(parts, breadcrumbSeparatorStyle.Render(" > "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return breadcrumbStyle.Render(strings.Join(parts, ""))
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/calmcacil/wg-admin/internal/config"
|
"github.com/calmcacil/wg-admin/internal/config"
|
||||||
"github.com/calmcacil/wg-admin/internal/validation"
|
"github.com/calmcacil/wg-admin/internal/validation"
|
||||||
"github.com/calmcacil/wg-admin/internal/wireguard"
|
"github.com/calmcacil/wg-admin/internal/wireguard"
|
||||||
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
@@ -13,8 +14,10 @@ import (
|
|||||||
|
|
||||||
// AddScreen is a form for adding new WireGuard clients
|
// AddScreen is a form for adding new WireGuard clients
|
||||||
type AddScreen struct {
|
type AddScreen struct {
|
||||||
form *huh.Form
|
form *huh.Form
|
||||||
quitting bool
|
quitting bool
|
||||||
|
spinner spinner.Model
|
||||||
|
isCreating bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ func (s *DetailScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
|
|||||||
s.transferTx = msg.transferTx
|
s.transferTx = msg.transferTx
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "b", "esc":
|
case "q", "b", "esc":
|
||||||
// Return to list screen - signal parent to switch screens
|
// Return to list screen - signal parent to switch screens
|
||||||
return nil, nil
|
return nil, nil
|
||||||
case "d":
|
case "d":
|
||||||
@@ -174,9 +174,9 @@ func (s *DetailScreen) View() string {
|
|||||||
func (s *DetailScreen) renderContent() string {
|
func (s *DetailScreen) renderContent() string {
|
||||||
statusText := s.status
|
statusText := s.status
|
||||||
if s.status == wireguard.StatusConnected {
|
if s.status == wireguard.StatusConnected {
|
||||||
statusText = detailConnectedStyle.Render(s.status)
|
statusText = detailConnectedStyle.Render("● " + s.status)
|
||||||
} else {
|
} else {
|
||||||
statusText = detailDisconnectedStyle.Render(s.status)
|
statusText = detailDisconnectedStyle.Render("● " + s.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build content
|
// Build content
|
||||||
@@ -205,7 +205,7 @@ func (s *DetailScreen) renderContent() string {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Add help text
|
// Add help text
|
||||||
helpText := detailHelpStyle.Render("Actions: [d] Delete • [c] View Config • [b] Back")
|
helpText := detailHelpStyle.Render("Actions: [d] Delete • [c] View Config • [q/b] Back")
|
||||||
content = lipgloss.JoinVertical(lipgloss.Left, content, helpText)
|
content = lipgloss.JoinVertical(lipgloss.Left, content, helpText)
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ func (s *HelpScreen) View() string {
|
|||||||
actionsGroup := categoryStyle.Render("Actions") + "\n" +
|
actionsGroup := categoryStyle.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("D") + descStyle.Render("Client details") + "\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")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package screens
|
|||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/calmcacil/wg-admin/internal/tui/components"
|
"github.com/calmcacil/wg-admin/internal/tui/components"
|
||||||
"github.com/calmcacil/wg-admin/internal/wireguard"
|
"github.com/calmcacil/wg-admin/internal/wireguard"
|
||||||
@@ -11,16 +12,17 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
const statusRefreshInterval = 10 // seconds
|
const statusRefreshInterval = 3 // seconds
|
||||||
|
|
||||||
// ListScreen displays a table of WireGuard clients
|
// ListScreen displays a table of WireGuard clients
|
||||||
type ListScreen struct {
|
type ListScreen struct {
|
||||||
table table.Model
|
table table.Model
|
||||||
search *components.SearchModel
|
search *components.SearchModel
|
||||||
clients []ClientWithStatus
|
clients []ClientWithStatus
|
||||||
filtered []ClientWithStatus
|
filtered []ClientWithStatus
|
||||||
sortedBy string // Column name being sorted by
|
sortedBy string // Column name being sorted by
|
||||||
ascending bool // Sort direction
|
ascending bool // Sort direction
|
||||||
|
lastUpdated time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientWithStatus wraps a client with its connection status
|
// ClientWithStatus wraps a client with its connection status
|
||||||
@@ -43,9 +45,20 @@ func (s *ListScreen) Init() tea.Cmd {
|
|||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
s.loadClients,
|
s.loadClients,
|
||||||
wireguard.Tick(statusRefreshInterval),
|
wireguard.Tick(statusRefreshInterval),
|
||||||
|
ticker(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ticker sends a message every second to update the time display
|
||||||
|
func ticker() tea.Cmd {
|
||||||
|
return tea.Tick(time.Second, func(t time.Time) tea.Msg {
|
||||||
|
return timeTickMsg(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// timeTickMsg is sent every second to update the time display
|
||||||
|
type timeTickMsg time.Time
|
||||||
|
|
||||||
// Update handles messages for the list screen
|
// Update handles messages for the list screen
|
||||||
func (s *ListScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
|
func (s *ListScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
@@ -104,8 +117,11 @@ func (s *ListScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
case clientsLoadedMsg:
|
case clientsLoadedMsg:
|
||||||
s.clients = msg.clients
|
s.clients = msg.clients
|
||||||
|
s.lastUpdated = time.Now()
|
||||||
s.search.SetTotalCount(len(s.clients))
|
s.search.SetTotalCount(len(s.clients))
|
||||||
s.applyFilter()
|
s.applyFilter()
|
||||||
|
case timeTickMsg:
|
||||||
|
// Trigger a re-render to update "Last updated" display
|
||||||
case wireguard.StatusTickMsg:
|
case wireguard.StatusTickMsg:
|
||||||
// Refresh status on periodic tick
|
// Refresh status on periodic tick
|
||||||
return s, s.loadClients
|
return s, s.loadClients
|
||||||
@@ -132,7 +148,29 @@ func (s *ListScreen) View() string {
|
|||||||
Render("No matching clients found. Try a different search term.")
|
Render("No matching clients found. Try a different search term.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.search.View() + "\n" + s.table.View()
|
// Calculate time since last update
|
||||||
|
timeAgo := "never"
|
||||||
|
if !s.lastUpdated.IsZero() {
|
||||||
|
duration := time.Since(s.lastUpdated)
|
||||||
|
timeAgo = formatDuration(duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUpdatedText := lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("241")).
|
||||||
|
Render("Last updated: " + timeAgo)
|
||||||
|
|
||||||
|
return s.search.View() + "\n" + s.table.View() + "\n" + lastUpdatedText
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatDuration returns a human-readable string for the duration
|
||||||
|
func formatDuration(d time.Duration) string {
|
||||||
|
if d < time.Minute {
|
||||||
|
return "just now"
|
||||||
|
}
|
||||||
|
if d < time.Hour {
|
||||||
|
return fmt.Sprintf("%d min ago", int(d.Minutes()))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d hr ago", int(d.Hours()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadClients loads clients from wireguard config
|
// loadClients loads clients from wireguard config
|
||||||
|
|||||||
Reference in New Issue
Block a user