Add keyboard shortcut discoverability hints on each screen
This commit is contained in:
@@ -186,8 +186,8 @@ func (s *DetailScreen) renderContent() string {
|
|||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add help text
|
// Add help text with all keyboard shortcuts
|
||||||
helpText := theme.StyleHelpKey.MarginTop(1).Render("Actions: [d] Delete • [c] View Config • [q/b] Back")
|
helpText := theme.StyleHelpKey.MarginTop(1).Render("Shortcuts: [d] Delete • [c] View Config • [q/b/esc] Back")
|
||||||
content = lipgloss.JoinVertical(lipgloss.Left, content, helpText)
|
content = lipgloss.JoinVertical(lipgloss.Left, content, helpText)
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|||||||
@@ -204,7 +204,14 @@ func (s *ListScreen) View() string {
|
|||||||
Foreground(lipgloss.Color("241")).
|
Foreground(lipgloss.Color("241")).
|
||||||
Render("Last updated: " + timeAgo)
|
Render("Last updated: " + timeAgo)
|
||||||
|
|
||||||
return breadcrumb + "\n" + s.search.View() + "\n" + s.table.View() + "\n" + lastUpdatedText
|
|
||||||
|
// Add keyboard shortcuts help
|
||||||
|
helpText := lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("63")).
|
||||||
|
MarginTop(1).
|
||||||
|
Render("Shortcuts: [?/h] Help • [a] Add • [R] Restore • [r] Refresh • [Q] QR Code • [/] Search • [1-4] Sort • [Enter] Details • [q] Quit")
|
||||||
|
|
||||||
|
return breadcrumb + "\n" + s.search.View() + "\n" + s.table.View() + "\n" + lastUpdatedText + "\n" + helpText
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatDuration returns a human-readable string for the duration
|
// formatDuration returns a human-readable string for the duration
|
||||||
|
|||||||
220
internal/tui/screens/list_go_append.txt
Normal file
220
internal/tui/screens/list_go_append.txt
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
lastUpdatedText := lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("241")).
|
||||||
|
Render("Last updated: " + timeAgo)
|
||||||
|
|
||||||
|
// Add keyboard shortcuts help
|
||||||
|
helpText := lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("63")).
|
||||||
|
MarginTop(1).
|
||||||
|
Render("Shortcuts: [?/h] Help • [a] Add • [R] Restore • [r] Refresh • [Q] QR Code • [/] Search • [1-4] Sort • [Enter] Details • [q] Quit")
|
||||||
|
|
||||||
|
return breadcrumb + "\n" + s.search.View() + "\n" + s.table.View() + "\n" + lastUpdatedText + "\n" + helpText
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
func (s *ListScreen) loadClients() tea.Msg {
|
||||||
|
clients, err := wireguard.ListClients()
|
||||||
|
if err != nil {
|
||||||
|
return ErrMsg{Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get status for each client
|
||||||
|
clientsWithStatus := make([]ClientWithStatus, len(clients))
|
||||||
|
for i, client := range clients {
|
||||||
|
status, err := wireguard.GetClientStatus(client.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
status = wireguard.StatusDisconnected
|
||||||
|
}
|
||||||
|
clientsWithStatus[i] = ClientWithStatus{
|
||||||
|
Client: client,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientsLoadedMsg{clients: clientsWithStatus}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyFilter applies the current search filter to clients
|
||||||
|
func (s *ListScreen) applyFilter() {
|
||||||
|
// Convert clients to ClientData for filtering
|
||||||
|
clientData := make([]components.ClientData, len(s.clients))
|
||||||
|
for i, cws := range s.clients {
|
||||||
|
clientData[i] = components.ClientData{
|
||||||
|
Name: cws.Client.Name,
|
||||||
|
IPv4: cws.Client.IPv4,
|
||||||
|
IPv6: cws.Client.IPv6,
|
||||||
|
Status: cws.Status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter clients
|
||||||
|
filteredData := s.search.Filter(clientData)
|
||||||
|
|
||||||
|
// Convert back to ClientWithStatus
|
||||||
|
s.filtered = make([]ClientWithStatus, len(filteredData))
|
||||||
|
for i, cd := range filteredData {
|
||||||
|
// Find the matching client
|
||||||
|
for _, cws := range s.clients {
|
||||||
|
if cws.Client.Name == cd.Name {
|
||||||
|
s.filtered[i] = cws
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild table with filtered clients
|
||||||
|
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
|
||||||
|
func (s *ListScreen) buildTable() {
|
||||||
|
columns := []table.Column{
|
||||||
|
{Title: "Name", Width: 20},
|
||||||
|
{Title: "IPv4", Width: 15},
|
||||||
|
{Title: "IPv6", Width: 35},
|
||||||
|
{Title: "Status", Width: 14},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use filtered clients if search is active, otherwise use all clients
|
||||||
|
displayClients := s.filtered
|
||||||
|
if !s.search.IsActive() {
|
||||||
|
displayClients = s.clients
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []table.Row
|
||||||
|
for _, cws := range displayClients {
|
||||||
|
statusText := s.formatStatusWithIcon(cws.Status)
|
||||||
|
row := table.Row{
|
||||||
|
cws.Client.Name,
|
||||||
|
cws.Client.IPv4,
|
||||||
|
cws.Client.IPv6,
|
||||||
|
statusText,
|
||||||
|
}
|
||||||
|
rows = append(rows, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort rows based on current sort settings
|
||||||
|
s.sortRows(rows)
|
||||||
|
|
||||||
|
// Determine table height
|
||||||
|
tableHeight := len(rows) + 2 // Header + rows
|
||||||
|
if tableHeight < 5 {
|
||||||
|
tableHeight = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
s.table = table.New(
|
||||||
|
table.WithColumns(columns),
|
||||||
|
table.WithRows(rows),
|
||||||
|
table.WithFocused(true),
|
||||||
|
table.WithHeight(tableHeight),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Apply styles
|
||||||
|
s.setTableStyles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// setTableStyles applies styling to the table
|
||||||
|
func (s *ListScreen) setTableStyles() {
|
||||||
|
styles := table.DefaultStyles()
|
||||||
|
styles.Header = styles.Header.
|
||||||
|
BorderStyle(lipgloss.NormalBorder()).
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortRows sorts the rows based on the current sort settings
|
||||||
|
func (s *ListScreen) sortRows(rows []table.Row) {
|
||||||
|
colIndex := s.getColumnIndex(s.sortedBy)
|
||||||
|
|
||||||
|
sort.Slice(rows, func(i, j int) bool {
|
||||||
|
var valI, valJ string
|
||||||
|
if colIndex < len(rows[i]) {
|
||||||
|
valI = rows[i][colIndex]
|
||||||
|
}
|
||||||
|
if colIndex < len(rows[j]) {
|
||||||
|
valJ = rows[j][colIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ascending {
|
||||||
|
return strings.ToLower(valI) < strings.ToLower(valJ)
|
||||||
|
}
|
||||||
|
return strings.ToLower(valI) > strings.ToLower(valJ)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortByColumn changes the sort column
|
||||||
|
func (s *ListScreen) sortByColumn(col string) {
|
||||||
|
sortedBy := "Name"
|
||||||
|
switch col {
|
||||||
|
case "1":
|
||||||
|
sortedBy = "Name"
|
||||||
|
case "2":
|
||||||
|
sortedBy = "IPv4"
|
||||||
|
case "3":
|
||||||
|
sortedBy = "IPv6"
|
||||||
|
case "4":
|
||||||
|
sortedBy = "Status"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle direction if clicking same column
|
||||||
|
if s.sortedBy == sortedBy {
|
||||||
|
s.ascending = !s.ascending
|
||||||
|
} else {
|
||||||
|
s.sortedBy = sortedBy
|
||||||
|
s.ascending = true
|
||||||
|
}
|
||||||
|
|
||||||
|
s.buildTable()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getColumnIndex returns the index of a column by name
|
||||||
|
func (s *ListScreen) getColumnIndex(name string) int {
|
||||||
|
switch name {
|
||||||
|
case "Name":
|
||||||
|
return 0
|
||||||
|
case "IPv4":
|
||||||
|
return 1
|
||||||
|
case "IPv6":
|
||||||
|
return 2
|
||||||
|
case "Status":
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
|
||||||
|
// clientsLoadedMsg is sent when clients are loaded
|
||||||
|
type clientsLoadedMsg struct {
|
||||||
|
clients []ClientWithStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrMsg is sent when an error occurs
|
||||||
|
type ErrMsg struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user