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}