1package dialog
2
3import (
4 "charm.land/bubbles/v2/key"
5 tea "charm.land/bubbletea/v2"
6 "charm.land/lipgloss/v2"
7 "github.com/charmbracelet/crush/internal/ui/common"
8 uv "github.com/charmbracelet/ultraviolet"
9)
10
11// CloseKey is the default key binding to close dialogs.
12var CloseKey = key.NewBinding(
13 key.WithKeys("esc", "alt+esc"),
14 key.WithHelp("esc", "exit"),
15)
16
17// Dialog is a component that can be displayed on top of the UI.
18type Dialog interface {
19 ID() string
20 Update(msg tea.Msg) tea.Cmd
21 View() string
22}
23
24// Overlay manages multiple dialogs as an overlay.
25type Overlay struct {
26 dialogs []Dialog
27}
28
29// NewOverlay creates a new [Overlay] instance.
30func NewOverlay(dialogs ...Dialog) *Overlay {
31 return &Overlay{
32 dialogs: dialogs,
33 }
34}
35
36// IsFrontDialog checks if the dialog with the specified ID is at the front.
37func (d *Overlay) IsFrontDialog(dialogID string) bool {
38 if len(d.dialogs) == 0 {
39 return false
40 }
41 return d.dialogs[len(d.dialogs)-1].ID() == dialogID
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// RemoveDialog removes the dialog with the specified ID from the stack.
65func (d *Overlay) RemoveDialog(dialogID string) {
66 for i, dialog := range d.dialogs {
67 if dialog.ID() == dialogID {
68 d.removeDialog(i)
69 return
70 }
71 }
72}
73
74// Dialog returns the dialog with the specified ID, or nil if not found.
75func (d *Overlay) Dialog(dialogID string) Dialog {
76 for _, dialog := range d.dialogs {
77 if dialog.ID() == dialogID {
78 return dialog
79 }
80 }
81 return nil
82}
83
84// BringToFront brings the dialog with the specified ID to the front.
85func (d *Overlay) BringToFront(dialogID string) {
86 for i, dialog := range d.dialogs {
87 if dialog.ID() == dialogID {
88 // Move the dialog to the end of the slice
89 d.dialogs = append(d.dialogs[:i], d.dialogs[i+1:]...)
90 d.dialogs = append(d.dialogs, dialog)
91 return
92 }
93 }
94}
95
96// Update handles dialog updates.
97func (d *Overlay) Update(msg tea.Msg) (*Overlay, tea.Cmd) {
98 if len(d.dialogs) == 0 {
99 return d, nil
100 }
101
102 idx := len(d.dialogs) - 1 // active dialog is the last one
103 dialog := d.dialogs[idx]
104 switch msg := msg.(type) {
105 case tea.KeyPressMsg:
106 if key.Matches(msg, CloseKey) {
107 // Close the current dialog
108 d.removeDialog(idx)
109 return d, nil
110 }
111 }
112
113 if cmd := dialog.Update(msg); cmd != nil {
114 // Close the current dialog
115 d.removeDialog(idx)
116 return d, cmd
117 }
118
119 return d, nil
120}
121
122// Draw renders the overlay and its dialogs.
123func (d *Overlay) Draw(scr uv.Screen, area uv.Rectangle) {
124 for _, dialog := range d.dialogs {
125 view := dialog.View()
126 viewWidth := lipgloss.Width(view)
127 viewHeight := lipgloss.Height(view)
128 center := common.CenterRect(area, viewWidth, viewHeight)
129 if area.Overlaps(center) {
130 uv.NewStyledString(view).Draw(scr, center)
131 }
132 }
133}
134
135// removeDialog removes a dialog from the stack.
136func (d *Overlay) removeDialog(idx int) {
137 if idx < 0 || idx >= len(d.dialogs) {
138 return
139 }
140 d.dialogs = append(d.dialogs[:idx], d.dialogs[idx+1:]...)
141}