mirror of
https://github.com/lukaszraczylo/lolcathost.git
synced 2026-10-12 04:55:13 +00:00
Switch imports to the charm.land v2 modules. Return tea.View from Model.View, set alt screen and window title there, use KeyPressMsg and the "space" key name, and call SetWidth on text inputs.
343 lines
7.1 KiB
Go
343 lines
7.1 KiB
Go
// Package tui provides the form component for adding/editing entries.
|
|
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"charm.land/bubbles/v2/textinput"
|
|
tea "charm.land/bubbletea/v2"
|
|
"github.com/lukaszraczylo/lolcathost/internal/config"
|
|
)
|
|
|
|
// 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].SetWidth(inputWidth)
|
|
}
|
|
}
|
|
|
|
// Update handles input events.
|
|
func (f *Form) Update(msg tea.Msg) tea.Cmd {
|
|
switch msg := msg.(type) {
|
|
case tea.KeyPressMsg:
|
|
// 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"
|
|
}
|
|
|
|
// Check if domain is blocked
|
|
if config.IsBlockedDomain(domain) {
|
|
return fmt.Sprintf("Domain '%s' is blocked (Apple system domain)", domain)
|
|
}
|
|
|
|
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(WrapHelpText("Tab/↓ next • Shift+Tab/↑ prev • ←→ select group • Enter save • Esc cancel", f.width-6))
|
|
|
|
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
|
|
}
|