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