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	util.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) (util.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) View() string {
102	return ""
103}
104
105func (d dialogCmp) handleOpen(msg OpenDialogMsg) (util.Model, tea.Cmd) {
106	if d.HasDialogs() {
107		dialog := d.dialogs[len(d.dialogs)-1]
108		if dialog.ID() == msg.Model.ID() {
109			return d, nil // Do not open a dialog if it's already the topmost one
110		}
111		if dialog.ID() == "quit" {
112			return d, nil // Do not open dialogs on top of quit
113		}
114	}
115	// if the dialog is already in the stack make it the last item
116	if _, ok := d.idMap[msg.Model.ID()]; ok {
117		existing := d.dialogs[d.idMap[msg.Model.ID()]]
118		// Reuse the model so we keep the state
119		msg.Model = existing
120		d.dialogs = slices.Delete(d.dialogs, d.idMap[msg.Model.ID()], d.idMap[msg.Model.ID()]+1)
121	}
122	d.idMap[msg.Model.ID()] = len(d.dialogs)
123	d.dialogs = append(d.dialogs, msg.Model)
124	var cmds []tea.Cmd
125	cmd := msg.Model.Init()
126	cmds = append(cmds, cmd)
127	_, cmd = msg.Model.Update(tea.WindowSizeMsg{
128		Width:  d.width,
129		Height: d.height,
130	})
131	cmds = append(cmds, cmd)
132	return d, tea.Batch(cmds...)
133}
134
135func (d dialogCmp) Dialogs() []DialogModel {
136	return d.dialogs
137}
138
139func (d dialogCmp) ActiveModel() util.Model {
140	if len(d.dialogs) == 0 {
141		return nil
142	}
143	return d.dialogs[len(d.dialogs)-1]
144}
145
146func (d dialogCmp) ActiveDialogID() DialogID {
147	if len(d.dialogs) == 0 {
148		return ""
149	}
150	return d.dialogs[len(d.dialogs)-1].ID()
151}
152
153func (d dialogCmp) GetLayers() []*lipgloss.Layer {
154	layers := []*lipgloss.Layer{}
155	for _, dialog := range d.Dialogs() {
156		dialogView := dialog.View()
157		row, col := dialog.Position()
158		layers = append(layers, lipgloss.NewLayer(dialogView).X(col).Y(row))
159	}
160	return layers
161}
162
163func (d dialogCmp) HasDialogs() bool {
164	return len(d.dialogs) > 0
165}