dialog.go

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