6 tea "charm.land/bubbletea/v2"
7 "charm.land/lipgloss/v2"
8 "github.com/charmbracelet/crush/internal/tui/util"
13// DialogModel represents a dialog component that can be displayed.
14type DialogModel interface {
20// CloseCallback allows dialogs to perform cleanup when closed.
21type CloseCallback interface {
25// OpenDialogMsg is sent to open a new dialog with specified dimensions.
26type OpenDialogMsg struct {
30// CloseDialogMsg is sent to close the topmost dialog.
31type CloseDialogMsg struct{}
33// DialogCmp manages a stack of dialogs with keyboard navigation.
34type DialogCmp interface {
37 Dialogs() []DialogModel
39 GetLayers() []*lipgloss.Layer
40 ActiveModel() util.Model
41 ActiveDialogID() DialogID
44type dialogCmp struct {
47 idMap map[DialogID]int
51// NewDialogCmp creates a new dialog manager.
52func NewDialogCmp() DialogCmp {
54 dialogs: []DialogModel{},
55 keyMap: DefaultKeyMap(),
56 idMap: make(map[DialogID]int),
60func (d dialogCmp) Init() tea.Cmd {
64// Update handles dialog lifecycle and forwards messages to the active dialog.
65func (d dialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
66 switch msg := msg.(type) {
67 case tea.WindowSizeMsg:
71 for i := range d.dialogs {
72 u, cmd := d.dialogs[i].Update(msg)
73 d.dialogs[i] = u.(DialogModel)
74 cmds = append(cmds, cmd)
76 return d, tea.Batch(cmds...)
78 return d.handleOpen(msg)
80 if len(d.dialogs) == 0 {
83 inx := len(d.dialogs) - 1
84 dialog := d.dialogs[inx]
85 delete(d.idMap, dialog.ID())
86 d.dialogs = d.dialogs[:len(d.dialogs)-1]
87 if closeable, ok := dialog.(CloseCallback); ok {
88 return d, closeable.Close()
93 lastIndex := len(d.dialogs) - 1
94 u, cmd := d.dialogs[lastIndex].Update(msg)
95 d.dialogs[lastIndex] = u.(DialogModel)
101func (d dialogCmp) View() string {
105func (d dialogCmp) handleOpen(msg OpenDialogMsg) (util.Model, tea.Cmd) {
107 dialog := d.dialogs[len(d.dialogs)-1]
108 if dialog.ID() == msg.Model.ID() {
109 return d, nil // Do not open a dialog if it's already the topmost one
111 if dialog.ID() == "quit" {
112 return d, nil // Do not open dialogs on top of quit
115 // if the dialog is already in the stack make it the last item
116 if _, ok := d.idMap[msg.Model.ID()]; ok {
117 existing := d.dialogs[d.idMap[msg.Model.ID()]]
118 // Reuse the model so we keep the state
120 d.dialogs = slices.Delete(d.dialogs, d.idMap[msg.Model.ID()], d.idMap[msg.Model.ID()]+1)
122 d.idMap[msg.Model.ID()] = len(d.dialogs)
123 d.dialogs = append(d.dialogs, msg.Model)
125 cmd := msg.Model.Init()
126 cmds = append(cmds, cmd)
127 _, cmd = msg.Model.Update(tea.WindowSizeMsg{
131 cmds = append(cmds, cmd)
132 return d, tea.Batch(cmds...)
135func (d dialogCmp) Dialogs() []DialogModel {
139func (d dialogCmp) ActiveModel() util.Model {
140 if len(d.dialogs) == 0 {
143 return d.dialogs[len(d.dialogs)-1]
146func (d dialogCmp) ActiveDialogID() DialogID {
147 if len(d.dialogs) == 0 {
150 return d.dialogs[len(d.dialogs)-1].ID()
153func (d dialogCmp) GetLayers() []*lipgloss.Layer {
154 layers := []*lipgloss.Layer{}
155 for _, dialog := range d.Dialogs() {
156 dialogView := dialog.View()
157 row, col := dialog.Position()
158 layers = append(layers, lipgloss.NewLayer(dialogView).X(col).Y(row))
163func (d dialogCmp) HasDialogs() bool {
164 return len(d.dialogs) > 0
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list