-
Notifications
You must be signed in to change notification settings - Fork 2
/
model.go
181 lines (153 loc) · 4.69 KB
/
model.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
// Package Navstack manages a stack of NavigationItems which can be pushed or popped from the stack.
// The top most stack navigation item is used by [BubbleTea] to Update and renders it's View.
// When pushing and popping items from the stack, the new view to be presented is sent a tea.WindowSizeMsg
// to ensure it's view can be presented correctly. When the last item is popped from the stack the application will quit.
// NavigationItem models which implement the Closable interface will have their Close method called when they are popped from the stack.
// This is useful for cleaning up resources that may not be garbage collected when a view a no longer needed.
// [BubbleTea]: https://github.com/charmbracelet/bubbletea
package navstack
import (
"errors"
tea "github.com/charmbracelet/bubbletea"
"github.com/kevm/bubbleo/window"
)
// Closable is an interface for models that have resources that need to be cleaned up when
// they are no longer needed. The navigation stack checks for this interface when popping items.
type Closable interface {
Close() error
}
type Model struct {
stack []NavigationItem
window *window.Model
}
// New creates a new navigation stack model. The window is used to
// constrain the view within the container of the navigation stack.
func New(w *window.Model) Model {
model := Model{
stack: []NavigationItem{},
window: w,
}
return model
}
func (m Model) Init() tea.Cmd {
top := m.Top()
if top == nil {
return nil
}
return top.Init()
}
// Push pushes a new navigation item onto the stack.
// The new navigation item is given a tea.WindowSizeMsg to ensure it's view can be presented correctly.
// The item's Init method is called and resulting command is processed by [BubbleTea].
// If top item's model implements the Closable interface the Close method is called.
// This new item will be the top most item on the stack and thus will be rendered.
func (m *Model) Push(item NavigationItem) tea.Cmd {
top := m.Top()
if top != nil {
if c, ok := top.Model.(Closable); ok {
c.Close()
}
}
initCmd := item.Init()
wmsg := m.window.GetWindowSizeMsg()
nim, winCmd := item.Model.Update(wmsg)
item.Model = nim
m.stack = append(m.stack, item)
return tea.Sequence(initCmd, winCmd)
}
// Pop removes the top most navigation item from the stack.
// If the item implements the Closable interface the Close method is called.
// The new top most item on the stack is given a tea.WindowSizeMsg to ensure it's view can be presented correctly.
// If there are no more items on the stack the application will quit.
func (m *Model) Pop() tea.Cmd {
top := m.Top()
if top == nil {
return tea.Quit // should not happen
}
if c, ok := top.Model.(Closable); ok {
c.Close()
}
m.stack = m.stack[:len(m.stack)-1]
top = m.Top()
if top == nil {
return tea.Quit
}
initCmd := top.Init()
nim, winCmd := top.Model.Update(m.window.GetWindowSizeMsg())
top.Model = nim
return tea.Sequence(winCmd, initCmd)
}
// Clear pops all the items from the stack.
func (m *Model) Clear() error {
var errs []error
for _, item := range m.stack {
if c, ok := item.Model.(Closable); ok {
err := c.Close()
if err != nil {
errs = append(errs, err)
}
}
}
m.stack = []NavigationItem{}
return errors.Join(errs...)
}
// Top returns the top most navigation item on the stack.
func (m Model) Top() *NavigationItem {
if len(m.stack) == 0 {
return nil
}
top := m.stack[len(m.stack)-1]
return &top
}
// StackSummary returns a list of titles for each item on the stack.
// This is currently used by the breadcrumb component to render the breadcrumb trail.
func (m Model) StackSummary() []string {
summary := []string{}
for _, item := range m.stack {
summary = append(summary, item.Title)
}
return summary
}
// Update processes messages for the top most navigation item on the stack.
func (m *Model) Update(msg tea.Msg) tea.Cmd {
top := m.Top()
switch msg := msg.(type) {
case tea.WindowSizeMsg: // update the window size based on offsets
if top == nil {
return nil
}
m.window.Height = msg.Height
m.window.Width = msg.Width
msg.Width = m.window.Width - m.window.SideOffset
msg.Height = m.window.Height - m.window.TopOffset
um, cmd := top.Update(msg)
m.stack[len(m.stack)-1] = um.(NavigationItem)
return cmd
case ReloadCurrent:
if top == nil {
return nil
}
return top.Init()
case PopNavigation:
cmd := m.Pop()
return cmd
case PushNavigation:
cmd := m.Push(msg.Item)
return cmd
default:
if top == nil {
return nil
}
um, cmd := top.Update(msg)
m.stack[len(m.stack)-1] = um.(NavigationItem)
return cmd
}
}
// View renders the top most navigation item on the stack.
func (m Model) View() string {
top := m.Top()
if top == nil {
return ""
}
return top.View()
}