1package dialog
2
3import (
4 "charm.land/bubbles/v2/key"
5 tea "charm.land/bubbletea/v2"
6 "charm.land/lipgloss/v2"
7)
8
9// CloseKey is the default key binding to close dialogs.
10var CloseKey = key.NewBinding(
11 key.WithKeys("esc", "alt+esc"),
12 key.WithHelp("esc", "exit"),
13)
14
15// OverlayKeyMap defines key bindings for dialogs.
16type OverlayKeyMap struct {
17 Close key.Binding
18}
19
20// ActionType represents the type of action taken by a dialog.
21type ActionType int
22
23const (
24 // ActionNone indicates no action.
25 ActionNone ActionType = iota
26 // ActionClose indicates that the dialog should be closed.
27 ActionClose
28 // ActionSelect indicates that an item has been selected.
29 ActionSelect
30)
31
32// Action represents an action taken by a dialog.
33// It can be used to signal closing or other operations.
34type Action struct {
35 Type ActionType
36 Payload any
37}
38
39// Dialog is a component that can be displayed on top of the UI.
40type Dialog interface {
41 ID() string
42 Update(msg tea.Msg) (Action, tea.Cmd)
43 Layer() *lipgloss.Layer
44}
45
46// Overlay manages multiple dialogs as an overlay.
47type Overlay struct {
48 dialogs []Dialog
49}
50
51// NewOverlay creates a new [Overlay] instance.
52func NewOverlay(dialogs ...Dialog) *Overlay {
53 return &Overlay{
54 dialogs: dialogs,
55 }
56}
57
58// IsFrontDialog checks if the dialog with the specified ID is at the front.
59func (d *Overlay) IsFrontDialog(dialogID string) bool {
60 if len(d.dialogs) == 0 {
61 return false
62 }
63 return d.dialogs[len(d.dialogs)-1].ID() == dialogID
64}
65
66// HasDialogs checks if there are any active dialogs.
67func (d *Overlay) HasDialogs() bool {
68 return len(d.dialogs) > 0
69}
70
71// ContainsDialog checks if a dialog with the specified ID exists.
72func (d *Overlay) ContainsDialog(dialogID string) bool {
73 for _, dialog := range d.dialogs {
74 if dialog.ID() == dialogID {
75 return true
76 }
77 }
78 return false
79}
80
81// AddDialog adds a new dialog to the stack.
82func (d *Overlay) AddDialog(dialog Dialog) {
83 d.dialogs = append(d.dialogs, dialog)
84}
85
86// RemoveDialog removes the dialog with the specified ID from the stack.
87func (d *Overlay) RemoveDialog(dialogID string) {
88 for i, dialog := range d.dialogs {
89 if dialog.ID() == dialogID {
90 d.removeDialog(i)
91 return
92 }
93 }
94}
95
96// BringToFront brings the dialog with the specified ID to the front.
97func (d *Overlay) BringToFront(dialogID string) {
98 for i, dialog := range d.dialogs {
99 if dialog.ID() == dialogID {
100 // Move the dialog to the end of the slice
101 d.dialogs = append(d.dialogs[:i], d.dialogs[i+1:]...)
102 d.dialogs = append(d.dialogs, dialog)
103 return
104 }
105 }
106}
107
108// Update handles dialog updates.
109func (d *Overlay) Update(msg tea.Msg) (*Overlay, tea.Cmd) {
110 if len(d.dialogs) == 0 {
111 return d, nil
112 }
113
114 idx := len(d.dialogs) - 1 // active dialog is the last one
115 dialog := d.dialogs[idx]
116 switch msg := msg.(type) {
117 case tea.KeyPressMsg:
118 if key.Matches(msg, CloseKey) {
119 // Close the current dialog
120 d.removeDialog(idx)
121 return d, nil
122 }
123 }
124
125 action, cmd := dialog.Update(msg)
126 switch action.Type {
127 case ActionClose:
128 // Close the current dialog
129 d.removeDialog(idx)
130 return d, cmd
131 case ActionSelect:
132 // Pass the action up (without modifying the dialog stack)
133 return d, cmd
134 }
135
136 return d, cmd
137}
138
139// Layers returns the current stack of dialogs as lipgloss layers.
140func (d *Overlay) Layers() []*lipgloss.Layer {
141 layers := make([]*lipgloss.Layer, len(d.dialogs))
142 for i, dialog := range d.dialogs {
143 layers[i] = dialog.Layer()
144 }
145 return layers
146}
147
148// removeDialog removes a dialog from the stack.
149func (d *Overlay) removeDialog(idx int) {
150 if idx < 0 || idx >= len(d.dialogs) {
151 return
152 }
153 d.dialogs = append(d.dialogs[:idx], d.dialogs[idx+1:]...)
154}