dialog.go

  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}