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