Files
wg-admin/internal/tui/screens/add.go

191 lines
4.2 KiB
Go

package screens
import (
"fmt"
"github.com/calmcacil/wg-admin/internal/config"
"github.com/calmcacil/wg-admin/internal/validation"
"github.com/calmcacil/wg-admin/internal/wireguard"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
)
// AddScreen is a form for adding new WireGuard clients
type AddScreen struct {
form *huh.Form
quitting bool
spinner spinner.Model
isCreating bool
}
// Styles
var (
addTitleStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("62")).
Bold(true).
MarginBottom(1)
addHelpStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("241")).
MarginTop(1)
addLoadingStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("62")).
Bold(true).
MarginTop(1)
)
// NewAddScreen creates a new add screen
func NewAddScreen() *AddScreen {
// Get default DNS from config
cfg, err := config.LoadConfig()
defaultDNS := "8.8.8.8, 8.8.4.4"
if err == nil && cfg.DNSServers != "" {
defaultDNS = cfg.DNSServers
}
// Create the form
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Key("name").
Title("Client Name").
Description("Name for the new client (alphanumeric, -, _)").
Placeholder("e.g., laptop-john").
Validate(func(s string) error {
return validation.ValidateClientName(s)
}),
huh.NewInput().
Key("dns").
Title("DNS Servers").
Description("Comma-separated IPv4 addresses").
Placeholder("e.g., 8.8.8.8, 8.8.4.4").
Value(&defaultDNS).
Validate(func(s string) error {
return validation.ValidateDNSServers(s)
}),
huh.NewConfirm().
Key("use_psk").
Title("Use Preshared Key").
Description("Enable additional security layer with a preshared key").
Affirmative("Yes").
Negative("No"),
),
)
// Create spinner for loading states
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("62"))
return &AddScreen{
form: form,
quitting: false,
spinner: s,
isCreating: false,
}
}
// Init initializes the add screen
func (s *AddScreen) Init() tea.Cmd {
if s.isCreating {
return s.spinner.Tick
}
return s.form.Init()
}
// Update handles messages for the add screen
func (s *AddScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c", "esc":
// Cancel and return to list (only if not creating)
if !s.isCreating {
return nil, nil
}
}
}
// If creating, update spinner instead of form
if s.isCreating {
var cmd tea.Cmd
s.spinner, cmd = s.spinner.Update(msg)
return s, cmd
}
// Update the form
form, cmd := s.form.Update(msg)
if f, ok := form.(*huh.Form); ok {
s.form = f
}
cmds = append(cmds, cmd)
// Check if form is completed
if s.form.State == huh.StateCompleted {
name := s.form.GetString("name")
dns := s.form.GetString("dns")
usePSK := s.form.GetBool("use_psk")
// Set creating state and start spinner
s.isCreating = true
// Create the client
return s, s.createClient(name, dns, usePSK)
}
return s, tea.Batch(cmds...)
}
// View renders the add screen
func (s *AddScreen) View() string {
if s.quitting {
return ""
}
if s.isCreating {
return addLoadingStyle.Render(
lipgloss.JoinVertical(
lipgloss.Left,
addTitleStyle.Render("Add New WireGuard Client"),
s.spinner.View()+" Creating client configuration, please wait...",
),
)
}
content := lipgloss.JoinVertical(
lipgloss.Left,
addTitleStyle.Render("Add New WireGuard Client"),
s.form.View(),
addHelpStyle.Render("Press Enter to submit • Esc to cancel"),
)
return content
}
// createClient creates a new WireGuard client
func (s *AddScreen) createClient(name, dns string, usePSK bool) tea.Cmd {
return func() tea.Msg {
// Create the client via wireguard package
err := wireguard.CreateClient(name, dns, usePSK)
if err != nil {
return ErrMsg{Err: fmt.Errorf("failed to create client: %w", err)}
}
// Return success message
return ClientCreatedMsg{
Name: name,
}
}
}
// Messages
// ClientCreatedMsg is sent when a client is successfully created
type ClientCreatedMsg struct {
Name string
}