package tui import ( "fmt" "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/lerko/nib/internal/carddata" ) type fillSlot struct { Name string Default string Value string } type fillModel struct { slots []fillSlot active int body string entityID string ti textinput.Model } func newFillModel(entityID, body string) fillModel { slots := discoverSlots(body) m := fillModel{ slots: slots, body: body, entityID: entityID, } m.ti = textinput.New() m.ti.CharLimit = 200 if len(slots) > 0 { m.ti.Placeholder = slots[0].Name m.ti.Focus() if slots[0].Default != "" { m.ti.SetValue(slots[0].Default) } } return m } func discoverSlots(body string) []fillSlot { matches := carddata.TemplateSlotRe.FindAllStringSubmatch(body, -1) seen := map[string]bool{} var slots []fillSlot for _, m := range matches { name := m[1] if !seen[name] { seen[name] = true slots = append(slots, fillSlot{Name: name}) } } return slots } func (f fillModel) resolve() string { result := f.body for _, s := range f.slots { val := s.Value if val == "" { val = "${" + s.Name + "}" } result = strings.ReplaceAll(result, "${"+s.Name+"}", val) } return result } func (f fillModel) update(msg tea.KeyMsg) (fillModel, tea.Cmd) { switch msg.String() { case "tab": f.commitActive() if f.active < len(f.slots)-1 { f.active++ } else { f.active = 0 } f.loadActive() return f, nil case "shift+tab": f.commitActive() if f.active > 0 { f.active-- } else { f.active = len(f.slots) - 1 } f.loadActive() return f, nil } f.ti, _ = f.ti.Update(msg) return f, nil } func (f *fillModel) commitActive() { if f.active < len(f.slots) { f.slots[f.active].Value = f.ti.Value() } } func (f *fillModel) loadActive() { if f.active < len(f.slots) { s := f.slots[f.active] f.ti.SetValue(s.Value) f.ti.Placeholder = s.Name f.ti.Focus() } } func (f fillModel) view(width int) string { if len(f.slots) == 0 { return statusStyle.Render("no slots") } var b strings.Builder header := fmt.Sprintf("⤓ filling slot %d/%d", f.active+1, len(f.slots)) b.WriteString(detailHeaderStyle.Render(header)) b.WriteString("\n\n") for i, slot := range f.slots { name := detailLabelStyle.Render(slot.Name) var val string if i == f.active { val = f.ti.View() } else if slot.Value != "" { val = detailValueStyle.Render(slot.Value) } else { val = idStyle.Render("(empty)") } if i == f.active { b.WriteString(selectedItemStyle.Render(" " + name + " " + val)) } else { b.WriteString(listItemStyle.Render(name + " " + val)) } b.WriteString("\n") } b.WriteString("\n") preview := f.resolve() if len(preview) > width-4 { preview = preview[:width-7] + "…" } b.WriteString(detailBodyStyle.Render(preview)) b.WriteString("\n\n") b.WriteString(helpStyle.Render("tab:next shift+tab:prev enter:copy esc:cancel")) return b.String() }