Compare commits

..

2 Commits

Author SHA1 Message Date
Calmcacil
b7ddd54cf6 bd sync: 2026-01-12 23:34:33 2026-01-12 23:34:33 +01:00
Calmcacil
5b8b9b66f5 Add keyboard shortcut discoverability hints on each screen 2026-01-12 23:34:14 +01:00
4 changed files with 231 additions and 4 deletions

View File

@@ -36,7 +36,7 @@
{"id":"wg-admin-ka8","title":"Generate QR codes for clients","description":"Generate ANSI-colored QR codes from client configs using qrterminal library. Support both inline and fullscreen QR display modes. Handle terminal resize events for optimal QR rendering.","status":"closed","priority":2,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T17:03:30.273562645+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T17:48:32.013388859+01:00","closed_at":"2026-01-12T17:48:32.013388859+01:00","close_reason":"QR code display implemented using qrterminal. Supports inline and fullscreen modes with f toggle. Handles terminal resize events. Returns to list on q/Esc.","dependencies":[{"issue_id":"wg-admin-ka8","depends_on_id":"wg-admin-wf1","type":"blocks","created_at":"2026-01-12T17:04:36.203581002+01:00","created_by":"Calmcacil"}]} {"id":"wg-admin-ka8","title":"Generate QR codes for clients","description":"Generate ANSI-colored QR codes from client configs using qrterminal library. Support both inline and fullscreen QR display modes. Handle terminal resize events for optimal QR rendering.","status":"closed","priority":2,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T17:03:30.273562645+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T17:48:32.013388859+01:00","closed_at":"2026-01-12T17:48:32.013388859+01:00","close_reason":"QR code display implemented using qrterminal. Supports inline and fullscreen modes with f toggle. Handles terminal resize events. Returns to list on q/Esc.","dependencies":[{"issue_id":"wg-admin-ka8","depends_on_id":"wg-admin-wf1","type":"blocks","created_at":"2026-01-12T17:04:36.203581002+01:00","created_by":"Calmcacil"}]}
{"id":"wg-admin-kfs","title":"Create configuration file format for WireGuard settings","description":"Design and implement /etc/wg-admin/config file to replace hardcoded values. Include: SERVER_DOMAIN, WG_PORT, VPN_IPV4_RANGE, VPN_IPV6_RANGE, WG_INTERFACE, DNS_SERVERS, and other configurable parameters. Support both file-based and environment variable override.","status":"closed","priority":2,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T16:27:53.148859434+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T16:31:29.339557739+01:00","closed_at":"2026-01-12T16:31:29.339557739+01:00","close_reason":"Config file approach replaced with interactive prompts using 'read', with 'WGI_' prefixed environment variable overrides. No persistent config file needed."} {"id":"wg-admin-kfs","title":"Create configuration file format for WireGuard settings","description":"Design and implement /etc/wg-admin/config file to replace hardcoded values. Include: SERVER_DOMAIN, WG_PORT, VPN_IPV4_RANGE, VPN_IPV6_RANGE, WG_INTERFACE, DNS_SERVERS, and other configurable parameters. Support both file-based and environment variable override.","status":"closed","priority":2,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T16:27:53.148859434+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T16:31:29.339557739+01:00","closed_at":"2026-01-12T16:31:29.339557739+01:00","close_reason":"Config file approach replaced with interactive prompts using 'read', with 'WGI_' prefixed environment variable overrides. No persistent config file needed."}
{"id":"wg-admin-lzl","title":"Add improved error handling and traps","description":"Implement: EXIT trap for cleanup on script interruption, pre-install validation (disk space, port availability, root check), rollback mechanism for failed operations, better error messages with actionable guidance, log all operations with timestamps.","status":"closed","priority":2,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T16:27:53.154445252+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T16:44:11.575490008+01:00","closed_at":"2026-01-12T16:44:11.575490008+01:00","close_reason":"Implemented all error handling and trap features: EXIT trap for cleanup (cleanup_handler catches EXIT,INT,TERM,HUP), pre-install validation (pre_install_validation checks disk space, port availability, root), rollback mechanism (rollback_installation function with BACKUP_DIR), better error messages with actionable guidance (all errors include specific fix suggestions), and logging with timestamps (log_info, log_error, log_warn functions)"} {"id":"wg-admin-lzl","title":"Add improved error handling and traps","description":"Implement: EXIT trap for cleanup on script interruption, pre-install validation (disk space, port availability, root check), rollback mechanism for failed operations, better error messages with actionable guidance, log all operations with timestamps.","status":"closed","priority":2,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T16:27:53.154445252+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T16:44:11.575490008+01:00","closed_at":"2026-01-12T16:44:11.575490008+01:00","close_reason":"Implemented all error handling and trap features: EXIT trap for cleanup (cleanup_handler catches EXIT,INT,TERM,HUP), pre-install validation (pre_install_validation checks disk space, port availability, root), rollback mechanism (rollback_installation function with BACKUP_DIR), better error messages with actionable guidance (all errors include specific fix suggestions), and logging with timestamps (log_info, log_error, log_warn functions)"}
{"id":"wg-admin-nyp","title":"Add keyboard shortcut discoverability hints on each screen","description":"Show available keyboard shortcuts on each screen in footer or help overlay. Users currently must remember or check help. Display context-sensitive shortcuts based on current screen and current state (e.g., when search is active).","status":"in_progress","priority":3,"issue_type":"feature","owner":"Calmcacil@Raion","created_at":"2026-01-12T21:40:48.826752641+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T23:22:49.898621171+01:00"} {"id":"wg-admin-nyp","title":"Add keyboard shortcut discoverability hints on each screen","description":"Show available keyboard shortcuts on each screen in footer or help overlay. Users currently must remember or check help. Display context-sensitive shortcuts based on current screen and current state (e.g., when search is active).","status":"closed","priority":3,"issue_type":"feature","owner":"Calmcacil@Raion","created_at":"2026-01-12T21:40:48.826752641+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T23:34:28.571338299+01:00","closed_at":"2026-01-12T23:34:28.571338299+01:00","close_reason":"Added keyboard shortcut hints to ListScreen and DetailScreen"}
{"id":"wg-admin-o4o","title":"Implement WireGuard key generation","description":"Implement WireGuard key generation using wg genkey and wg pubkey commands. Generate client private key, public key, and optional pre-shared key (PSK). Ensure atomic file writes and proper permissions (0600).","status":"closed","priority":2,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T17:03:30.283256646+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T17:51:24.471200438+01:00","closed_at":"2026-01-12T17:51:24.471200438+01:00","close_reason":"Implemented WireGuard key generation in internal/wireguard/keys.go with GeneratePrivateKey(), GeneratePublicKey(), GeneratePSK(), GenerateKeyPair(), StoreKey(), LoadKey(), ValidateKey(), and CleanupTempKeys() functions. Uses wg genkey, wg pubkey, and wg genpsk commands with atomic writes (temp file + mv) and 0600 permissions. Includes key validation (44 base64 chars) and temp key tracking for cleanup. Package builds successfully.","dependencies":[{"issue_id":"wg-admin-o4o","depends_on_id":"wg-admin-wod","type":"blocks","created_at":"2026-01-12T17:04:52.815358118+01:00","created_by":"Calmcacil"}]} {"id":"wg-admin-o4o","title":"Implement WireGuard key generation","description":"Implement WireGuard key generation using wg genkey and wg pubkey commands. Generate client private key, public key, and optional pre-shared key (PSK). Ensure atomic file writes and proper permissions (0600).","status":"closed","priority":2,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T17:03:30.283256646+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T17:51:24.471200438+01:00","closed_at":"2026-01-12T17:51:24.471200438+01:00","close_reason":"Implemented WireGuard key generation in internal/wireguard/keys.go with GeneratePrivateKey(), GeneratePublicKey(), GeneratePSK(), GenerateKeyPair(), StoreKey(), LoadKey(), ValidateKey(), and CleanupTempKeys() functions. Uses wg genkey, wg pubkey, and wg genpsk commands with atomic writes (temp file + mv) and 0600 permissions. Includes key validation (44 base64 chars) and temp key tracking for cleanup. Package builds successfully.","dependencies":[{"issue_id":"wg-admin-o4o","depends_on_id":"wg-admin-wod","type":"blocks","created_at":"2026-01-12T17:04:52.815358118+01:00","created_by":"Calmcacil"}]}
{"id":"wg-admin-onv","title":"Enhance search with match highlighting, count display, Ctrl+U to clear","description":"Improve search component with: highlight matching text in results, show number of matches found, add Ctrl+U shortcut to clear search, Tab to cycle filter types. Better filtering experience with clear exit from search mode.","status":"in_progress","priority":3,"issue_type":"feature","owner":"Calmcacil@Raion","created_at":"2026-01-12T21:40:48.818862183+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T23:22:38.765877552+01:00"} {"id":"wg-admin-onv","title":"Enhance search with match highlighting, count display, Ctrl+U to clear","description":"Improve search component with: highlight matching text in results, show number of matches found, add Ctrl+U shortcut to clear search, Tab to cycle filter types. Better filtering experience with clear exit from search mode.","status":"in_progress","priority":3,"issue_type":"feature","owner":"Calmcacil@Raion","created_at":"2026-01-12T21:40:48.818862183+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T23:22:38.765877552+01:00"}
{"id":"wg-admin-p3o","title":"Set Everforest as default theme","description":"Change the default theme from 'default' to 'everforest' in the GetTheme function. This will make Everforest the default when no THEME environment variable is set.","status":"closed","priority":1,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T19:07:40.302533502+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T19:08:50.637133192+01:00","closed_at":"2026-01-12T19:08:50.637133192+01:00","close_reason":"Implemented Dracula and Everforest color schemes and set Everforest as default theme"} {"id":"wg-admin-p3o","title":"Set Everforest as default theme","description":"Change the default theme from 'default' to 'everforest' in the GetTheme function. This will make Everforest the default when no THEME environment variable is set.","status":"closed","priority":1,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T19:07:40.302533502+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T19:08:50.637133192+01:00","closed_at":"2026-01-12T19:08:50.637133192+01:00","close_reason":"Implemented Dracula and Everforest color schemes and set Everforest as default theme"}

View File

@@ -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

View File

@@ -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

View 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
}