1package dialogs
  2
  3import (
  4	"slices"
  5
  6	tea "github.com/charmbracelet/bubbletea/v2"
  7	"github.com/charmbracelet/crush/internal/tui/util"
  8	"github.com/charmbracelet/lipgloss/v2"
  9)
 10
 11type DialogID string
 12
 13// DialogModel represents a dialog component that can be displayed.
 14type DialogModel interface {
 15	util.Model
 16	Position() (int, int)
 17	ID() DialogID
 18}
 19
 20// CloseCallback allows dialogs to perform cleanup when closed.
 21type CloseCallback interface {
 22	Close() tea.Cmd
 23}
 24
 25// OpenDialogMsg is sent to open a new dialog with specified dimensions.
 26type OpenDialogMsg struct {
 27	Model DialogModel
 28}
 29
 30// CloseDialogMsg is sent to close the topmost dialog.
 31type CloseDialogMsg struct{}
 32
 33// DialogCmp manages a stack of dialogs with keyboard navigation.
 34type DialogCmp interface {
 35	tea.Model
 36
 37	Dialogs() []DialogModel
 38	HasDialogs() bool
 39	GetLayers() []*lipgloss.Layer
 40	ActiveModel() util.Model
 41	ActiveDialogID() DialogID
 42}
 43
 44type dialogCmp struct {
 45	width, height int
 46	dialogs       []DialogModel
 47	idMap         map[DialogID]int
 48	keyMap        KeyMap
 49}
 50
 51// NewDialogCmp creates a new dialog manager.
 52func NewDialogCmp() DialogCmp {
 53	return dialogCmp{
 54		dialogs: []DialogModel{},
 55		keyMap:  DefaultKeyMap(),
 56		idMap:   make(map[DialogID]int),
 57	}
 58}
 59
 60func (d dialogCmp) Init() tea.Cmd {
 61	return nil
 62}
 63
 64// Update handles dialog lifecycle and forwards messages to the active dialog.
 65func (d dialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 66	switch msg := msg.(type) {
 67	case tea.WindowSizeMsg:
 68		var cmds []tea.Cmd
 69		d.width = msg.Width
 70		d.height = msg.Height
 71		for i := range d.dialogs {
 72			u, cmd := d.dialogs[i].Update(msg)
 73			d.dialogs[i] = u.(DialogModel)
 74			cmds = append(cmds, cmd)
 75		}
 76		return d, tea.Batch(cmds...)
 77	case OpenDialogMsg:
 78		return d.handleOpen(msg)
 79	case CloseDialogMsg:
 80		if len(d.dialogs) == 0 {
 81			return d, nil
 82		}
 83		inx := len(d.dialogs) - 1
 84		dialog := d.dialogs[inx]
 85		delete(d.idMap, dialog.ID())
 86		d.dialogs = d.dialogs[:len(d.dialogs)-1]
 87		if closeable, ok := dialog.(CloseCallback); ok {
 88			return d, closeable.Close()
 89		}
 90		return d, nil
 91	}
 92	if d.HasDialogs() {
 93		lastIndex := len(d.dialogs) - 1
 94		u, cmd := d.dialogs[lastIndex].Update(msg)
 95		d.dialogs[lastIndex] = u.(DialogModel)
 96		return d, cmd
 97	}
 98	return d, nil
 99}
100
101func (d dialogCmp) handleOpen(msg OpenDialogMsg) (tea.Model, tea.Cmd) {
102	if d.HasDialogs() {
103		dialog := d.dialogs[len(d.dialogs)-1]
104		if dialog.ID() == msg.Model.ID() {
105			return d, nil // Do not open a dialog if it's already the topmost one
106		}
107		if dialog.ID() == "quit" {
108			return d, nil // Do not open dialogs on top of quit
109		}
110	}
111	// if the dialog is already in the stack make it the last item
112	if _, ok := d.idMap[msg.Model.ID()]; ok {
113		existing := d.dialogs[d.idMap[msg.Model.ID()]]
114		// Reuse the model so we keep the state
115		msg.Model = existing
116		d.dialogs = slices.Delete(d.dialogs, d.idMap[msg.Model.ID()], d.idMap[msg.Model.ID()]+1)
117	}
118	d.idMap[msg.Model.ID()] = len(d.dialogs)
119	d.dialogs = append(d.dialogs, msg.Model)
120	var cmds []tea.Cmd
121	cmd := msg.Model.Init()
122	cmds = append(cmds, cmd)
123	_, cmd = msg.Model.Update(tea.WindowSizeMsg{
124		Width:  d.width,
125		Height: d.height,
126	})
127	cmds = append(cmds, cmd)
128	return d, tea.Batch(cmds...)
129}
130
131func (d dialogCmp) Dialogs() []DialogModel {
132	return d.dialogs
133}
134
135func (d dialogCmp) ActiveModel() util.Model {
136	if len(d.dialogs) == 0 {
137		return nil
138	}
139	return d.dialogs[len(d.dialogs)-1]
140}
141
142func (d dialogCmp) ActiveDialogID() DialogID {
143	if len(d.dialogs) == 0 {
144		return ""
145	}
146	return d.dialogs[len(d.dialogs)-1].ID()
147}
148
149func (d dialogCmp) GetLayers() []*lipgloss.Layer {
150	layers := []*lipgloss.Layer{}
151	for _, dialog := range d.Dialogs() {
152		dialogView := dialog.View()
153		row, col := dialog.Position()
154		layers = append(layers, lipgloss.NewLayer(dialogView).X(col).Y(row))
155	}
156	return layers
157}
158
159func (d dialogCmp) HasDialogs() bool {
160	return len(d.dialogs) > 0
161}