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// BringToFront brings the dialog with the specified ID to the front.
75func (d *Overlay) BringToFront(dialogID string) {
76 for i, dialog := range d.dialogs {
77 if dialog.ID() == dialogID {
78 // Move the dialog to the end of the slice
79 d.dialogs = append(d.dialogs[:i], d.dialogs[i+1:]...)
80 d.dialogs = append(d.dialogs, dialog)
81 return
82 }
83 }
84}
85
86// Update handles dialog updates.
87func (d *Overlay) Update(msg tea.Msg) (*Overlay, tea.Cmd) {
88 if len(d.dialogs) == 0 {
89 return d, nil
90 }
91
92 idx := len(d.dialogs) - 1 // active dialog is the last one
93 dialog := d.dialogs[idx]
94 switch msg := msg.(type) {
95 case tea.KeyPressMsg:
96 if key.Matches(msg, CloseKey) {
97 // Close the current dialog
98 d.removeDialog(idx)
99 return d, nil
100 }
101 }
102
103 if cmd := dialog.Update(msg); cmd != nil {
104 // Close the current dialog
105 d.removeDialog(idx)
106 return d, cmd
107 }
108
109 return d, nil
110}
111
112// Draw renders the overlay and its dialogs.
113func (d *Overlay) Draw(scr uv.Screen, area uv.Rectangle) {
114 for _, dialog := range d.dialogs {
115 view := dialog.View()
116 viewWidth := lipgloss.Width(view)
117 viewHeight := lipgloss.Height(view)
118 center := common.CenterRect(area, viewWidth, viewHeight)
119 if area.Overlaps(center) {
120 uv.NewStyledString(view).Draw(scr, center)
121 }
122 }
123}
124
125// removeDialog removes a dialog from the stack.
126func (d *Overlay) removeDialog(idx int) {
127 if idx < 0 || idx >= len(d.dialogs) {
128 return
129 }
130 d.dialogs = append(d.dialogs[:idx], d.dialogs[idx+1:]...)
131}