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/component"
  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	component.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// ContainsDialog checks if a dialog with the specified ID exists.
 45func (d *Overlay) ContainsDialog(dialogID string) bool {
 46	for _, dialog := range d.dialogs {
 47		if dialog.ID() == dialogID {
 48			return true
 49		}
 50	}
 51	return false
 52}
 53
 54// AddDialog adds a new dialog to the stack.
 55func (d *Overlay) AddDialog(dialog Dialog) {
 56	d.dialogs = append(d.dialogs, dialog)
 57}
 58
 59// BringToFront brings the dialog with the specified ID to the front.
 60func (d *Overlay) BringToFront(dialogID string) {
 61	for i, dialog := range d.dialogs {
 62		if dialog.ID() == dialogID {
 63			// Move the dialog to the end of the slice
 64			d.dialogs = append(d.dialogs[:i], d.dialogs[i+1:]...)
 65			d.dialogs = append(d.dialogs, dialog)
 66			return
 67		}
 68	}
 69}
 70
 71// Update handles dialog updates.
 72func (d *Overlay) Update(msg tea.Msg) (*Overlay, tea.Cmd) {
 73	if len(d.dialogs) == 0 {
 74		return d, nil
 75	}
 76
 77	idx := len(d.dialogs) - 1 // active dialog is the last one
 78	dialog := d.dialogs[idx]
 79	switch msg := msg.(type) {
 80	case tea.KeyPressMsg:
 81		if key.Matches(msg, d.keyMap.Close) {
 82			// Close the current dialog
 83			d.removeDialog(idx)
 84			return d, nil
 85		}
 86	}
 87
 88	updatedDialog, cmd := dialog.Update(msg)
 89	if updatedDialog == nil {
 90		// Dialog requested to be closed
 91		d.removeDialog(idx)
 92		return d, cmd
 93	}
 94
 95	// Update the dialog in the stack
 96	d.dialogs[idx] = updatedDialog
 97
 98	return d, cmd
 99}
100
101// View implements [Model].
102func (d *Overlay) View() string {
103	if len(d.dialogs) == 0 {
104		return ""
105	}
106
107	// Compose all the dialogs into a single view
108	canvas := lipgloss.NewCanvas()
109	for _, dialog := range d.dialogs {
110		layer := lipgloss.NewLayer(dialog.View())
111		canvas.AddLayers(layer)
112	}
113
114	return canvas.Render()
115}
116
117// ShortHelp implements [help.KeyMap].
118func (d *Overlay) ShortHelp() []key.Binding {
119	return []key.Binding{
120		d.keyMap.Close,
121	}
122}
123
124// FullHelp implements [help.KeyMap].
125func (d *Overlay) FullHelp() [][]key.Binding {
126	return [][]key.Binding{
127		{d.keyMap.Close},
128	}
129}
130
131// removeDialog removes a dialog from the stack.
132func (d *Overlay) removeDialog(idx int) {
133	if idx < 0 || idx >= len(d.dialogs) {
134		return
135	}
136	d.dialogs = append(d.dialogs[:idx], d.dialogs[idx+1:]...)
137}