diff --git a/cmd/wg-tui/main.go b/cmd/wg-tui/main.go new file mode 100644 index 0000000..5a0db99 --- /dev/null +++ b/cmd/wg-tui/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "fmt" + "os" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/calmcacil/wg-admin/internal/config" + "github.com/calmcacil/wg-admin/internal/tui/screens" +) + +const version = "0.1.0" + +var ( + styleTitle = lipgloss.NewStyle().Foreground(lipgloss.Color("62")).Bold(true) + styleSubtitle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")) + styleSuccess = lipgloss.NewStyle().Foreground(lipgloss.Color("46")) + styleError = lipgloss.NewStyle().Foreground(lipgloss.Color("196")) + styleHelpKey = lipgloss.NewStyle().Foreground(lipgloss.Color("63")).Bold(true) +) + +type model struct { + currentScreen screens.Screen + previousScreen screens.Screen + quitting bool + initialized bool +} + +func (m model) Init() tea.Cmd { + if os.Geteuid() != 0 { + fmt.Println(styleError.Render("ERROR: Must run as root")) + os.Exit(1) + } + _, _ = config.LoadConfig() + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // Handle initialization on first update after Init + if !m.initialized { + m.initialized = true + m.currentScreen = screens.NewListScreen() + return m, m.currentScreen.Init() + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "q", "ctrl+c": + m.quitting = true + return m, tea.Quit + case "?": + // Switch to help screen + m.previousScreen = m.currentScreen + m.currentScreen = screens.NewHelpScreen(m.previousScreen) + return m, m.currentScreen.Init() + case "l": + // Switch to list screen + m.currentScreen = screens.NewListScreen() + return m, m.currentScreen.Init() + } + case screens.ClientSelectedMsg: + // User selected a client - show detail screen + m.previousScreen = m.currentScreen + m.currentScreen = screens.NewDetailScreen(msg.Client.Client) + return m, m.currentScreen.Init() + case screens.ClientDeletedMsg: + // Client was deleted - show success message and return to list + m.currentScreen = screens.NewListScreen() + return m, m.currentScreen.Init() + case screens.CloseDetailScreenMsg: + // Detail screen closed - go back to previous screen + if m.previousScreen != nil { + m.currentScreen = m.previousScreen + m.previousScreen = nil + return m, m.currentScreen.Init() + } + m.currentScreen = screens.NewListScreen() + return m, m.currentScreen.Init() + } + + // Pass messages to current screen + if m.currentScreen != nil { + newScreen, cmd := m.currentScreen.Update(msg) + // If screen returns nil, go back to previous screen + if newScreen == nil { + if m.previousScreen != nil { + m.currentScreen = m.previousScreen + m.previousScreen = nil + } + } else if newScreen != m.currentScreen { + // Screen is switching to a different screen + m.previousScreen = m.currentScreen + m.currentScreen = newScreen + } + return m, cmd + } + + return m, nil +} + +func (m model) View() string { + if m.quitting { + return "\nGoodbye!\n" + } + + if m.currentScreen != nil && m.initialized { + return m.currentScreen.View() + } + + title := styleTitle.Render("WireGuard Client Manager") + subtitle := styleSubtitle.Render(fmt.Sprintf("v%s", version)) + bar := fmt.Sprintf("Press %s for help, %s to quit", styleHelpKey.Render("?"), styleHelpKey.Render("q")) + + return title + " " + subtitle + "\n\nInitializing...\n\n" + bar +} + +func main() { + p := tea.NewProgram(model{}, tea.WithAltScreen()) + if _, err := p.Run(); err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } +} diff --git a/internal/tui/screens/help.go b/internal/tui/screens/help.go new file mode 100644 index 0000000..cd22155 --- /dev/null +++ b/internal/tui/screens/help.go @@ -0,0 +1,112 @@ +package screens + +import ( + "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// HelpScreen displays keyboard shortcuts +type HelpScreen struct { + previousScreen Screen +} + +// NewHelpScreen creates a new help screen +func NewHelpScreen(previous Screen) *HelpScreen { + return &HelpScreen{ + previousScreen: previous, + } +} + +// Init initializes the help screen +func (s *HelpScreen) Init() tea.Cmd { + return nil +} + +// Update handles messages for the help screen +func (s *HelpScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "q", "esc": + // Return to previous screen + return s.previousScreen, nil + } + } + return s, nil +} + +// View renders the help screen +func (s *HelpScreen) View() string { + // Styles + borderStyle := lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("62")). + Padding(1, 2) + + headerStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("62")). + Bold(true). + MarginBottom(1) + + categoryStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("226")). + Bold(true). + MarginTop(1). + MarginBottom(0) + + keyStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("63")). + Bold(true). + Width(12) + + descStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("250")) + + // Header + header := headerStyle.Render("Keyboard Shortcuts") + + // Shortcut groups + navigationGroup := categoryStyle.Render("Navigation") + "\n" + + keyStyle.Render("j / ↓") + descStyle.Render("Move down") + "\n" + + keyStyle.Render("k / ↑") + descStyle.Render("Move up") + "\n" + + keyStyle.Render("← →") + descStyle.Render("Move left/right") + "\n" + + keyStyle.Render("Enter") + descStyle.Render("Select") + "\n" + + keyStyle.Render("Esc") + descStyle.Render("Go back") + + actionsGroup := categoryStyle.Render("Actions") + "\n" + + keyStyle.Render("a") + descStyle.Render("Add 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("r") + descStyle.Render("Refresh list") + "\n" + + keyStyle.Render("l") + descStyle.Render("List view") + + otherGroup := categoryStyle.Render("Other") + "\n" + + keyStyle.Render("?") + descStyle.Render("Show this help") + "\n" + + keyStyle.Render("/") + descStyle.Render("Search") + "\n" + + keyStyle.Render("q") + descStyle.Render("Quit") + + // Two-column layout + leftColumn := lipgloss.JoinVertical(lipgloss.Left, + navigationGroup, + "", + actionsGroup, + ) + + rightColumn := lipgloss.JoinVertical(lipgloss.Left, + otherGroup, + ) + + content := lipgloss.JoinHorizontal(lipgloss.Top, leftColumn, " ", rightColumn) + + // Footer + footerStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")). + MarginTop(1) + footer := footerStyle.Render("Press q or Esc to return") + + // Combine all + return borderStyle.Render( + lipgloss.JoinVertical(lipgloss.Left, header, content, footer), + ) +}