package adminui import ( "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // Tab extends tea.Model with an InForm indicator used by the root app to // suppress tab-switch shortcuts while a form is open. type Tab interface { tea.Model InForm() bool } var ( titleSt = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#00BFFF")) activeTS = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#39D353")) dimSt = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")) errSt = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF4444")) okSt = lipgloss.NewStyle().Foreground(lipgloss.Color("#39D353")) cursorSt = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFD700")) labelSt = lipgloss.NewStyle().Foreground(lipgloss.Color("#7EC8E3")) hdrSt = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#E94560")) ) const ( helpList = "a:add e:edit d:del j/k:nav r:reload q:quit" helpForm = "tab/shift+tab:nav ←/→:cycle ctrl+s:save esc:cancel" ) // prefill creates a pre-filled text field (value shown, not just placeholder). func prefill(label, value string) field { f := newTextField(label, "") f.input.SetValue(value) return f } // setEnumSel sets the sel index of an enum field to match value. func setEnumSel(f *field, value string) { for i, opt := range f.options { if opt == value { f.sel = i return } } } // field is a form entry: either a text input or an enum cycle selector. type field struct { label string isEnum bool input textinput.Model options []string sel int hidden bool } func newTextField(label, placeholder string) field { ti := textinput.New() ti.Placeholder = placeholder ti.CharLimit = 256 return field{label: label, input: ti} } func newPasswordField(label string) field { ti := textinput.New() ti.EchoMode = textinput.EchoPassword ti.Placeholder = "password" ti.CharLimit = 128 return field{label: label, input: ti} } func newEnumField(label string, options []string) field { return field{label: label, isEnum: true, options: options} } func (f *field) value() string { if f.isEnum { return f.options[f.sel] } return f.input.Value() } func (f *field) cycleNext() { if f.isEnum { f.sel = (f.sel + 1) % len(f.options) } } func (f *field) cyclePrev() { if f.isEnum { f.sel = (f.sel - 1 + len(f.options)) % len(f.options) } } // applyFocus focuses the field at idx and blurs all others. func applyFocus(fields []field, idx int) { for i := range fields { if !fields[i].isEnum { fields[i].input.Blur() } } if idx >= 0 && idx < len(fields) && !fields[idx].isEnum { fields[idx].input.Focus() } } // nextVisible returns the index of the next non-hidden field after from. func nextVisible(fields []field, from int) int { n := len(fields) for i := 1; i < n; i++ { idx := (from + i) % n if !fields[idx].hidden { return idx } } return from } // prevVisible returns the index of the previous non-hidden field before from. func prevVisible(fields []field, from int) int { n := len(fields) for i := 1; i < n; i++ { idx := (from - i + n) % n if !fields[idx].hidden { return idx } } return from } // handleFormKey routes a key message to the form: navigates fields or updates // the focused one. Returns (newFocusIdx, cmd). func handleFormKey(fields []field, focusIdx int, msg tea.KeyMsg) (int, tea.Cmd) { switch msg.String() { case "tab", "down": next := nextVisible(fields, focusIdx) applyFocus(fields, next) return next, nil case "shift+tab", "up": prev := prevVisible(fields, focusIdx) applyFocus(fields, prev) return prev, nil } // Route to focused field f := &fields[focusIdx] if f.isEnum { switch msg.String() { case "left", "h": f.cyclePrev() case "right", "l", " ": f.cycleNext() } return focusIdx, nil } var cmd tea.Cmd f.input, cmd = f.input.Update(msg) return focusIdx, cmd } // renderForm renders all non-hidden form fields. func renderForm(fields []field, focusIdx int) string { var sb strings.Builder for i := range fields { f := &fields[i] if f.hidden { continue } prefix := " " if i == focusIdx { prefix = cursorSt.Render("▸ ") } lbl := labelSt.Render(f.label + ":") if f.isEnum { var parts []string for j, opt := range f.options { if j == f.sel { parts = append(parts, activeTS.Render("["+opt+"]")) } else { parts = append(parts, dimSt.Render(" "+opt+" ")) } } sb.WriteString(prefix + lbl + " " + strings.Join(parts, "") + "\n") } else { sb.WriteString(prefix + lbl + " " + f.input.View() + "\n") } } return sb.String() } // renderStatus renders the status line (ok or error style). func renderStatus(msg string, isErr bool) string { if msg == "" { return "" } if isErr { return errSt.Render("✗ " + msg) } return okSt.Render("✓ " + msg) }