Files
lolcathost/internal/tui/form.go
T
2025-11-28 12:57:23 +00:00

337 lines
6.9 KiB
Go

// Package tui provides the form component for adding/editing entries.
package tui
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
)
// FormMode represents the form mode.
type FormMode int
const (
FormModeAdd FormMode = iota
FormModeEdit
)
// FormField represents a form field index.
type FormField int
const (
FieldDomain FormField = iota
FieldIP
FieldGroup
FieldCount
)
// Form handles the add/edit entry form.
type Form struct {
mode FormMode
fields []textinput.Model
focus FormField
width int
height int
editAlias string // Original alias when editing
// Group dropdown
groups []string
groupCursor int
groupFocused bool
}
// NewForm creates a new form.
func NewForm() *Form {
fields := make([]textinput.Model, FieldCount)
// Domain field
fields[FieldDomain] = textinput.New()
fields[FieldDomain].Placeholder = "example.com"
fields[FieldDomain].CharLimit = 253
// IP field
fields[FieldIP] = textinput.New()
fields[FieldIP].Placeholder = "127.0.0.1"
fields[FieldIP].CharLimit = 45 // IPv6 max
// Group field (not used as text input, but kept for compatibility)
fields[FieldGroup] = textinput.New()
fields[FieldGroup].Placeholder = "development"
fields[FieldGroup].CharLimit = 63
return &Form{
fields: fields,
focus: FieldDomain,
groups: []string{"default"},
}
}
// SetGroups sets the available groups for the dropdown.
func (f *Form) SetGroups(groups []string) {
if len(groups) == 0 {
f.groups = []string{"default"}
} else {
f.groups = groups
}
// Reset cursor if out of bounds
if f.groupCursor >= len(f.groups) {
f.groupCursor = 0
}
}
// Init initializes the form for adding a new entry.
func (f *Form) Init() {
f.mode = FormModeAdd
f.editAlias = ""
for i := range f.fields {
f.fields[i].Reset()
}
f.fields[FieldIP].SetValue("127.0.0.1")
f.groupCursor = 0
f.groupFocused = false
f.focus = FieldDomain
f.fields[FieldDomain].Focus()
}
// InitEdit initializes the form for editing an existing entry.
func (f *Form) InitEdit(domain, ip, alias, group string) {
f.mode = FormModeEdit
f.editAlias = alias
f.fields[FieldDomain].SetValue(domain)
f.fields[FieldIP].SetValue(ip)
// Find the group in the list
f.groupCursor = 0
for i, g := range f.groups {
if g == group {
f.groupCursor = i
break
}
}
f.groupFocused = false
f.focus = FieldDomain
f.fields[FieldDomain].Focus()
}
// SetSize sets the form dimensions.
func (f *Form) SetSize(width, height int) {
f.width = width
f.height = height
inputWidth := min(50, width-10)
for i := range f.fields {
f.fields[i].Width = inputWidth
}
}
// Update handles input events.
func (f *Form) Update(msg tea.Msg) tea.Cmd {
switch msg := msg.(type) {
case tea.KeyMsg:
// Handle group dropdown navigation
if f.focus == FieldGroup {
switch msg.String() {
case "tab":
f.nextField()
return nil
case "shift+tab":
f.prevField()
return nil
case "up", "k":
if f.groupCursor > 0 {
f.groupCursor--
}
return nil
case "down", "j":
if f.groupCursor < len(f.groups)-1 {
f.groupCursor++
}
return nil
case "left":
if f.groupCursor > 0 {
f.groupCursor--
}
return nil
case "right":
if f.groupCursor < len(f.groups)-1 {
f.groupCursor++
}
return nil
}
return nil
}
// Handle text input fields
switch msg.String() {
case "tab", "down":
f.nextField()
return nil
case "shift+tab", "up":
f.prevField()
return nil
}
}
// Update the focused text field (only for Domain and IP)
if f.focus != FieldGroup {
var cmd tea.Cmd
f.fields[f.focus], cmd = f.fields[f.focus].Update(msg)
return cmd
}
return nil
}
func (f *Form) nextField() {
if f.focus != FieldGroup {
f.fields[f.focus].Blur()
}
f.focus = (f.focus + 1) % FieldCount
if f.focus != FieldGroup {
f.fields[f.focus].Focus()
}
}
func (f *Form) prevField() {
if f.focus != FieldGroup {
f.fields[f.focus].Blur()
}
f.focus = (f.focus - 1 + FieldCount) % FieldCount
if f.focus != FieldGroup {
f.fields[f.focus].Focus()
}
}
// Values returns the form values (domain, ip, group).
func (f *Form) Values() (domain, ip, group string) {
group = ""
if f.groupCursor < len(f.groups) {
group = f.groups[f.groupCursor]
}
return strings.TrimSpace(f.fields[FieldDomain].Value()),
strings.TrimSpace(f.fields[FieldIP].Value()),
group
}
// EditAlias returns the original alias when editing.
func (f *Form) EditAlias() string {
return f.editAlias
}
// IsEdit returns true if in edit mode.
func (f *Form) IsEdit() bool {
return f.mode == FormModeEdit
}
// Validate validates the form values.
func (f *Form) Validate() string {
domain, ip, group := f.Values()
if domain == "" {
return "Domain is required"
}
if ip == "" {
return "IP address is required"
}
if group == "" {
return "Group is required"
}
return ""
}
// View renders the form.
func (f *Form) View() string {
var sb strings.Builder
title := "Add New Entry"
if f.mode == FormModeEdit {
title = "Edit Entry"
}
sb.WriteString(titleStyle.Render(title))
sb.WriteString("\n\n")
// Domain field
sb.WriteString(inputLabelStyle.Render("Domain:"))
sb.WriteString("\n")
style := inputStyle
if f.focus == FieldDomain {
style = inputFocusStyle
}
sb.WriteString(style.Render(f.fields[FieldDomain].View()))
sb.WriteString("\n\n")
// IP field
sb.WriteString(inputLabelStyle.Render("IP Address:"))
sb.WriteString("\n")
style = inputStyle
if f.focus == FieldIP {
style = inputFocusStyle
}
sb.WriteString(style.Render(f.fields[FieldIP].View()))
sb.WriteString("\n\n")
// Group dropdown
sb.WriteString(inputLabelStyle.Render("Group:"))
sb.WriteString("\n")
sb.WriteString(f.renderGroupDropdown())
sb.WriteString("\n\n")
sb.WriteString("\n")
sb.WriteString(helpDescStyle.Render("Tab/↓ next • Shift+Tab/↑ prev • ←→ select group • Enter save • Esc cancel"))
return dialogStyle.Render(sb.String())
}
func (f *Form) renderGroupDropdown() string {
isFocused := f.focus == FieldGroup
// Get current group name
currentGroup := "default"
if f.groupCursor < len(f.groups) {
currentGroup = f.groups[f.groupCursor]
}
// Build the selector content: ◀ group_name ▶
var content string
if isFocused {
// Show arrows when focused
leftArrow := "◀"
rightArrow := "▶"
if f.groupCursor == 0 {
leftArrow = " " // dim or hide left arrow at start
}
if f.groupCursor >= len(f.groups)-1 {
rightArrow = " " // dim or hide right arrow at end
}
content = leftArrow + " " + currentGroup + " " + rightArrow
} else {
content = " " + currentGroup + " "
}
// Show position indicator if multiple groups
if len(f.groups) > 1 {
content += fmt.Sprintf(" (%d/%d)", f.groupCursor+1, len(f.groups))
}
// Apply border style
if isFocused {
return inputFocusStyle.Render(content)
}
return inputStyle.Render(content)
}
func min(a, b int) int {
if a < b {
return a
}
return b
}