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