package tui import ( "fmt" "strconv" "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "git.tukangketik.id/swanadiva/hostkeeper/internal/models" ) type formMode int const ( formModeAdd formMode = iota formModeEdit ) type fieldID int const ( fieldName fieldID = iota fieldHostname fieldPort fieldUsername fieldAuthType fieldPassword fieldGroup fieldTags fieldNotes fieldCount ) var fieldLabels = map[fieldID]string{ fieldName: "Name", fieldHostname: "Hostname", fieldPort: "Port", fieldUsername: "Username", fieldAuthType: "Auth Type", fieldPassword: "Password", fieldGroup: "Group", fieldTags: "Tags", fieldNotes: "Notes", } // HostFormTab is a tab for adding/editing hosts type HostFormTab struct { mode formMode editing *models.Host dataDir string inputs []textinput.Model focus fieldID width int height int err error saved bool } // NewAddHostFormTab creates a new host add form tab func NewAddHostFormTab(dataDir string) *HostFormTab { return newHostFormTab(formModeAdd, nil, dataDir) } // NewEditHostFormTab creates a new host edit form tab func NewEditHostFormTab(host *models.Host, dataDir string) *HostFormTab { return newHostFormTab(formModeEdit, host, dataDir) } func newHostFormTab(mode formMode, host *models.Host, dataDir string) *HostFormTab { inputs := make([]textinput.Model, fieldCount) for i := range inputs { inputs[i] = textinput.New() inputs[i].Prompt = "" } inputs[fieldName].Placeholder = "My Server" inputs[fieldHostname].Placeholder = "192.168.1.1 or server.example.com" inputs[fieldPort].Placeholder = "22" inputs[fieldPort].SetValue("22") inputs[fieldUsername].Placeholder = "root" inputs[fieldAuthType].SetValue("password") inputs[fieldPassword].EchoMode = textinput.EchoPassword inputs[fieldPassword].Placeholder = "Enter password" inputs[fieldGroup].Placeholder = "production" inputs[fieldTags].Placeholder = "web,backend" inputs[fieldNotes].Placeholder = "Optional notes..." if mode == formModeEdit && host != nil { inputs[fieldName].SetValue(host.Name) inputs[fieldHostname].SetValue(host.Hostname) inputs[fieldPort].SetValue(strconv.Itoa(host.Port)) inputs[fieldUsername].SetValue(host.Username) inputs[fieldAuthType].SetValue(host.Auth.Type) if host.Auth.Password != "" { inputs[fieldPassword].SetValue(host.Auth.Password) } inputs[fieldGroup].SetValue(host.Group) inputs[fieldTags].SetValue(strings.Join(host.Tags, ",")) inputs[fieldNotes].SetValue(host.Notes) } inputs[fieldName].Focus() inputs[fieldName].Prompt = "> " return &HostFormTab{ mode: mode, editing: host, dataDir: dataDir, inputs: inputs, } } func (t *HostFormTab) Name() string { if t.mode == formModeEdit { return "Edit: " + t.editing.Name } return "Add Host" } func (t *HostFormTab) Init() tea.Cmd { return textinput.Blink } func (t *HostFormTab) Update(msg tea.Msg) (Tab, tea.Cmd) { if t.saved { return t, nil } switch msg := msg.(type) { case tea.WindowSizeMsg: t.width = msg.Width t.height = msg.Height case tea.KeyMsg: switch msg.String() { case "esc": return t, func() tea.Msg { return closeFormMsg{} } case "enter": if t.focus == fieldAuthType { t.toggleAuthType() return t, nil } if t.focus == fieldCount-1 { return t.submit() } t.nextField() case " ", "left", "right": if t.focus == fieldAuthType { t.toggleAuthType() return t, nil } fallthrough case "tab", "down": t.nextField() case "shift+tab", "up": t.prevField() case "ctrl+s": return t.submit() default: if t.focus == fieldAuthType { // ignore typing on auth type field return t, nil } var cmd tea.Cmd t.inputs[t.focus], cmd = t.inputs[t.focus].Update(msg) return t, cmd } } return t, nil } func (t *HostFormTab) nextField() { t.inputs[t.focus].Blur() t.inputs[t.focus].Prompt = "" t.focus++ if t.focus >= fieldCount { t.focus = fieldCount - 1 } t.inputs[t.focus].Focus() t.inputs[t.focus].Prompt = "> " } func (t *HostFormTab) prevField() { t.inputs[t.focus].Blur() t.inputs[t.focus].Prompt = "" t.focus-- if t.focus < 0 { t.focus = 0 } t.inputs[t.focus].Focus() t.inputs[t.focus].Prompt = "> " } func cycleAuthType(current string) string { switch current { case "password": return "key" case "key": return "password" default: return "password" } } func (t *HostFormTab) toggleAuthType() { current := t.inputs[fieldAuthType].Value() t.inputs[fieldAuthType].SetValue(cycleAuthType(current)) } func (t *HostFormTab) submit() (Tab, tea.Cmd) { name := t.inputs[fieldName].Value() hostname := t.inputs[fieldHostname].Value() username := t.inputs[fieldUsername].Value() if name == "" || hostname == "" || username == "" { t.err = fmt.Errorf("name, hostname, and username are required") return t, nil } port := 22 if p := t.inputs[fieldPort].Value(); p != "" { if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 { port = parsed } } authType := t.inputs[fieldAuthType].Value() if authType == "" { authType = "password" } var tags []string if tagStr := t.inputs[fieldTags].Value(); tagStr != "" { for _, tag := range strings.Split(tagStr, ",") { if trimmed := strings.TrimSpace(tag); trimmed != "" { tags = append(tags, trimmed) } } } var host *models.Host if t.mode == formModeEdit && t.editing != nil { host = t.editing host.Name = name host.Hostname = hostname host.Port = port host.Username = username host.Auth.Type = authType host.Auth.Password = t.inputs[fieldPassword].Value() host.Group = t.inputs[fieldGroup].Value() host.Tags = tags host.Notes = t.inputs[fieldNotes].Value() } else { host = &models.Host{ Name: name, Hostname: hostname, Port: port, Username: username, Auth: models.AuthConfig{ Type: authType, Password: t.inputs[fieldPassword].Value(), }, Group: t.inputs[fieldGroup].Value(), Tags: tags, Notes: t.inputs[fieldNotes].Value(), } } t.saved = true return t, saveHostCmd(host, t.dataDir) } func (t *HostFormTab) View() string { var b strings.Builder title := "Add New Host" if t.mode == formModeEdit { title = "Edit Host" } b.WriteString(AppTitleStyle.Render(title)) b.WriteString("\n\n") if t.err != nil { b.WriteString(ErrorStyle.Render(fmt.Sprintf("Error: %v", t.err))) b.WriteString("\n\n") } for i := fieldID(0); i < fieldCount; i++ { input := t.inputs[i] label := fieldLabels[i] style := SubtitleStyle if i == t.focus { style = HighlightStyle } b.WriteString(style.Render(label + ":")) b.WriteString("\n") if i == fieldAuthType { // Render auth type as toggle pills current := input.Value() pills := []string{"password", "key"} var parts []string for _, p := range pills { if p == current { if i == t.focus { parts = append(parts, SelectedStyle.Render(" "+p+" ")) } else { parts = append(parts, TagStyle.Render(" "+p+" ")) } } else { parts = append(parts, SubtitleStyle.Render(" "+p+" ")) } } b.WriteString(" ") b.WriteString(strings.Join(parts, " ")) b.WriteString("\n") if i == t.focus { b.WriteString(InfoStyle.Render(" Space/←/→ to toggle")) } b.WriteString("\n") } else { renderedInput := input.View() b.WriteString(" ") b.WriteString(renderedInput) b.WriteString("\n\n") } } // Help text b.WriteString(SubtitleStyle.Render( "↑↓: navigate Enter: next Ctrl+S: save Esc: cancel")) return b.String() } func (t *HostFormTab) Close() {}