@@ -0,0 +1,123 @@
1+package dialog
2+
3+import (
4+ "github.com/charmbracelet/bubbles/v2/key"
5+ tea "github.com/charmbracelet/bubbletea/v2"
6+ "github.com/charmbracelet/crush/internal/ui/common"
7+ "github.com/charmbracelet/lipgloss/v2"
8+)
9+
10+// Model is a component that can be displayed on top of the UI.
11+type Model interface {
12+ common.Model[Model]
13+ ID() string
14+}
15+
16+// Overlay manages multiple dialogs as an overlay.
17+type Overlay struct {
18+ dialogs []Model
19+ keyMap KeyMap
20+}
21+
22+// NewOverlay creates a new [Overlay] instance.
23+func NewOverlay(dialogs ...Model) *Overlay {
24+ return &Overlay{
25+ dialogs: dialogs,
26+ keyMap: DefaultKeyMap(),
27+ }
28+}
29+
30+// ContainsDialog checks if a dialog with the specified ID exists.
31+func (d *Overlay) ContainsDialog(dialogID string) bool {
32+ for _, dialog := range d.dialogs {
33+ if dialog.ID() == dialogID {
34+ return true
35+ }
36+ }
37+ return false
38+}
39+
40+// AddDialog adds a new dialog to the stack.
41+func (d *Overlay) AddDialog(dialog Model) {
42+ d.dialogs = append(d.dialogs, dialog)
43+}
44+
45+// BringToFront brings the dialog with the specified ID to the front.
46+func (d *Overlay) BringToFront(dialogID string) {
47+ for i, dialog := range d.dialogs {
48+ if dialog.ID() == dialogID {
49+ // Move the dialog to the end of the slice
50+ d.dialogs = append(d.dialogs[:i], d.dialogs[i+1:]...)
51+ d.dialogs = append(d.dialogs, dialog)
52+ return
53+ }
54+ }
55+}
56+
57+// Update handles dialog updates.
58+func (d *Overlay) Update(msg tea.Msg) (*Overlay, tea.Cmd) {
59+ if len(d.dialogs) == 0 {
60+ return d, nil
61+ }
62+
63+ idx := len(d.dialogs) - 1 // active dialog is the last one
64+ dialog := d.dialogs[idx]
65+ switch msg := msg.(type) {
66+ case tea.KeyPressMsg:
67+ if key.Matches(msg, d.keyMap.Close) {
68+ // Close the current dialog
69+ d.removeDialog(idx)
70+ return d, nil
71+ }
72+ }
73+
74+ updatedDialog, cmd := dialog.Update(msg)
75+ if updatedDialog == nil {
76+ // Dialog requested to be closed
77+ d.removeDialog(idx)
78+ return d, cmd
79+ }
80+
81+ // Update the dialog in the stack
82+ d.dialogs[idx] = updatedDialog
83+
84+ return d, cmd
85+}
86+
87+// View implements [Model].
88+func (d *Overlay) View() string {
89+ if len(d.dialogs) == 0 {
90+ return ""
91+ }
92+
93+ // Compose all the dialogs into a single view
94+ canvas := lipgloss.NewCanvas()
95+ for _, dialog := range d.dialogs {
96+ layer := lipgloss.NewLayer(dialog.View())
97+ canvas.AddLayers(layer)
98+ }
99+
100+ return canvas.Render()
101+}
102+
103+// ShortHelp implements [help.KeyMap].
104+func (d *Overlay) ShortHelp() []key.Binding {
105+ return []key.Binding{
106+ d.keyMap.Close,
107+ }
108+}
109+
110+// FullHelp implements [help.KeyMap].
111+func (d *Overlay) FullHelp() [][]key.Binding {
112+ return [][]key.Binding{
113+ {d.keyMap.Close},
114+ }
115+}
116+
117+// removeDialog removes a dialog from the stack.
118+func (d *Overlay) removeDialog(idx int) {
119+ if idx < 0 || idx >= len(d.dialogs) {
120+ return
121+ }
122+ d.dialogs = append(d.dialogs[:idx], d.dialogs[idx+1:]...)
123+}