Skip to content

Commit 179b092

Browse files
authored
Merge pull request #355 from rumpl/feature/multiple-dialogs-management
feat: Implement stack-based multiple dialogs management in TUI
2 parents 57a421b + 8ac3132 commit 179b092

File tree

2 files changed

+56
-49
lines changed

2 files changed

+56
-49
lines changed

pkg/tui/dialog/dialog.go

Lines changed: 54 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ type OpenDialogMsg struct {
1212
Model Dialog
1313
}
1414

15-
// CloseDialogMsg is sent to close the current dialog
15+
// CloseDialogMsg is sent to close the current (topmost) dialog
1616
type CloseDialogMsg struct{}
1717

18+
// CloseAllDialogsMsg is sent to close all dialogs in the stack
19+
type CloseAllDialogsMsg struct{}
20+
1821
// Dialog defines the interface that all dialogs must implement
1922
type Dialog interface {
2023
layout.Model
@@ -25,27 +28,20 @@ type Dialog interface {
2528
type Manager interface {
2629
tea.Model
2730

28-
GetLayer() *lipgloss.Layer
31+
GetLayers() []*lipgloss.Layer
2932
HasDialog() bool
3033
}
3134

3235
// manager implements Manager
3336
type manager struct {
3437
width, height int
35-
currentDialog Dialog // Single active dialog
36-
keyMap KeyMap // Global dialog key bindings
37-
}
38-
39-
// KeyMap defines global dialog key bindings
40-
type KeyMap struct {
41-
// Add any global dialog keys here if needed
38+
dialogStack []Dialog
4239
}
4340

4441
// New creates a new dialog component manager
4542
func New() Manager {
4643
return &manager{
47-
currentDialog: nil,
48-
keyMap: KeyMap{},
44+
dialogStack: make([]Dialog, 0),
4945
}
5046
}
5147

@@ -60,50 +56,56 @@ func (d *manager) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6056
case tea.WindowSizeMsg:
6157
d.width = msg.Width
6258
d.height = msg.Height
63-
// Propagate resize to current dialog if it exists
64-
if d.currentDialog != nil {
65-
u, cmd := d.currentDialog.Update(msg)
66-
d.currentDialog = u.(Dialog)
67-
return d, cmd
59+
// Propagate resize to all dialogs in the stack
60+
var cmds []tea.Cmd
61+
for i := range d.dialogStack {
62+
u, cmd := d.dialogStack[i].Update(msg)
63+
d.dialogStack[i] = u.(Dialog)
64+
if cmd != nil {
65+
cmds = append(cmds, cmd)
66+
}
6867
}
69-
return d, nil
68+
return d, tea.Batch(cmds...)
7069

7170
case OpenDialogMsg:
7271
return d.handleOpen(msg)
7372

7473
case CloseDialogMsg:
7574
return d.handleClose()
75+
76+
case CloseAllDialogsMsg:
77+
return d.handleCloseAll()
7678
}
7779

78-
// Forward messages to current dialog if it exists
79-
if d.currentDialog != nil {
80-
u, cmd := d.currentDialog.Update(msg)
81-
d.currentDialog = u.(Dialog)
80+
// Forward messages to top dialog if it exists
81+
// Only the topmost dialog receives input to prevent conflicts
82+
if len(d.dialogStack) > 0 {
83+
topIndex := len(d.dialogStack) - 1
84+
u, cmd := d.dialogStack[topIndex].Update(msg)
85+
d.dialogStack[topIndex] = u.(Dialog)
8286
return d, cmd
8387
}
8488
return d, nil
8589
}
8690

87-
// View renders the current dialog (used for debugging, actual rendering uses GetLayers)
91+
// View renders all dialogs (used for debugging, actual rendering uses GetLayers)
8892
func (d *manager) View() string {
8993
// This is mainly for debugging - actual rendering uses GetLayers
90-
if d.currentDialog == nil {
94+
if len(d.dialogStack) == 0 {
9195
return ""
9296
}
93-
return d.currentDialog.View()
97+
// Return view of top dialog for debugging
98+
return d.dialogStack[len(d.dialogStack)-1].View()
9499
}
95100

96-
// handleOpen processes dialog opening requests
101+
// handleOpen processes dialog opening requests and adds to stack
97102
func (d *manager) handleOpen(msg OpenDialogMsg) (tea.Model, tea.Cmd) {
98-
// Set the new dialog as current
99-
d.currentDialog = msg.Model
103+
d.dialogStack = append(d.dialogStack, msg.Model)
100104

101-
// Initialize dialog
102105
var cmds []tea.Cmd
103106
cmd := msg.Model.Init()
104107
cmds = append(cmds, cmd)
105108

106-
// Send initial window size
107109
_, cmd = msg.Model.Update(tea.WindowSizeMsg{
108110
Width: d.width,
109111
Height: d.height,
@@ -113,28 +115,39 @@ func (d *manager) handleOpen(msg OpenDialogMsg) (tea.Model, tea.Cmd) {
113115
return d, tea.Batch(cmds...)
114116
}
115117

116-
// handleClose processes dialog closing requests
118+
// handleClose processes dialog closing requests (pops top dialog from stack)
117119
func (d *manager) handleClose() (tea.Model, tea.Cmd) {
118-
if d.currentDialog == nil {
119-
return d, nil
120+
if len(d.dialogStack) != 0 {
121+
d.dialogStack = d.dialogStack[:len(d.dialogStack)-1]
120122
}
121123

122-
d.currentDialog = nil
124+
return d, nil
125+
}
123126

127+
// handleCloseAll closes all dialogs in the stack
128+
func (d *manager) handleCloseAll() (tea.Model, tea.Cmd) {
129+
d.dialogStack = make([]Dialog, 0)
124130
return d, nil
125131
}
126132

127-
// HasDialog returns true if there is an active dialog
133+
// HasDialog returns true if there is at least one active dialog
128134
func (d *manager) HasDialog() bool {
129-
return d.currentDialog != nil
135+
return len(d.dialogStack) > 0
130136
}
131137

132-
// GetLayer returns lipgloss layer for rendering the current dialog
133-
func (d *manager) GetLayer() *lipgloss.Layer {
134-
if d.currentDialog == nil {
138+
// GetLayers returns lipgloss layers for rendering all dialogs in the stack
139+
// Dialogs are returned in order from bottom to top (index 0 is bottom-most)
140+
func (d *manager) GetLayers() []*lipgloss.Layer {
141+
if len(d.dialogStack) == 0 {
135142
return nil
136143
}
137-
dialogView := d.currentDialog.View()
138-
row, col := d.currentDialog.Position()
139-
return lipgloss.NewLayer(dialogView).X(col).Y(row)
144+
145+
layers := make([]*lipgloss.Layer, 0, len(d.dialogStack))
146+
for _, dialog := range d.dialogStack {
147+
dialogView := dialog.View()
148+
row, col := dialog.Position()
149+
layers = append(layers, lipgloss.NewLayer(dialogView).X(col).Y(row))
150+
}
151+
152+
return layers
140153
}

pkg/tui/tui.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -247,19 +247,13 @@ func (a *appModel) View() tea.View {
247247

248248
baseView := lipgloss.JoinVertical(lipgloss.Top, components...)
249249

250-
// Create layered view if there is a dialog
251250
if a.dialog.HasDialog() {
252-
// Create background layer with the base view
253251
baseLayer := lipgloss.NewLayer(baseView)
252+
dialogLayers := a.dialog.GetLayers()
254253

255-
// Get dialog layers
256-
dialogLayer := a.dialog.GetLayer()
257-
258-
// Combine all layers
259254
allLayers := []*lipgloss.Layer{baseLayer}
260-
allLayers = append(allLayers, dialogLayer)
255+
allLayers = append(allLayers, dialogLayers...)
261256

262-
// Create and render canvas
263257
canvas := lipgloss.NewCanvas(allLayers...)
264258
return toFullscreenView(canvas.Render())
265259
}

0 commit comments

Comments
 (0)