1package repo
2
3import (
4 "fmt"
5
6 gitm "github.com/aymanbagabas/git-module"
7 "github.com/charmbracelet/bubbles/v2/key"
8 "github.com/charmbracelet/bubbles/v2/spinner"
9 tea "github.com/charmbracelet/bubbletea/v2"
10 lipgloss "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/soft-serve/git"
12 "github.com/charmbracelet/soft-serve/pkg/proto"
13 "github.com/charmbracelet/soft-serve/pkg/ui/common"
14 "github.com/charmbracelet/soft-serve/pkg/ui/components/code"
15 "github.com/charmbracelet/soft-serve/pkg/ui/components/selector"
16)
17
18type stashState int
19
20const (
21 stashStateLoading stashState = iota
22 stashStateList
23 stashStatePatch
24)
25
26// StashListMsg is a message sent when the stash list is loaded.
27type StashListMsg []*gitm.Stash
28
29// StashPatchMsg is a message sent when the stash patch is loaded.
30type StashPatchMsg struct{ *git.Diff }
31
32// Stash is the stash component page.
33type Stash struct {
34 common common.Common
35 code *code.Code
36 ref RefMsg
37 repo proto.Repository
38 spinner spinner.Model
39 list *selector.Selector
40 state stashState
41 currentPatch StashPatchMsg
42}
43
44// NewStash creates a new stash model.
45func NewStash(common common.Common) *Stash {
46 code := code.New(common, "", "")
47 s := spinner.New(spinner.WithSpinner(spinner.Dot),
48 spinner.WithStyle(common.Styles.Spinner))
49 selector := selector.New(common, []selector.IdentifiableItem{}, StashItemDelegate{&common})
50 selector.SetShowFilter(false)
51 selector.SetShowHelp(false)
52 selector.SetShowPagination(false)
53 selector.SetShowStatusBar(false)
54 selector.SetShowTitle(false)
55 selector.SetFilteringEnabled(false)
56 selector.DisableQuitKeybindings()
57 selector.KeyMap.NextPage = common.KeyMap.NextPage
58 selector.KeyMap.PrevPage = common.KeyMap.PrevPage
59 return &Stash{
60 code: code,
61 common: common,
62 spinner: s,
63 list: selector,
64 }
65}
66
67// Path implements common.TabComponent.
68func (s *Stash) Path() string {
69 return ""
70}
71
72// TabName returns the name of the tab.
73func (s *Stash) TabName() string {
74 return "Stash"
75}
76
77// SetSize implements common.Component.
78func (s *Stash) SetSize(width, height int) {
79 s.common.SetSize(width, height)
80 s.code.SetSize(width, height)
81 s.list.SetSize(width, height)
82}
83
84// ShortHelp implements help.KeyMap.
85func (s *Stash) ShortHelp() []key.Binding {
86 return []key.Binding{
87 s.common.KeyMap.Select,
88 s.common.KeyMap.Back,
89 s.common.KeyMap.UpDown,
90 }
91}
92
93// FullHelp implements help.KeyMap.
94func (s *Stash) FullHelp() [][]key.Binding {
95 b := [][]key.Binding{
96 {
97 s.common.KeyMap.Select,
98 s.common.KeyMap.Back,
99 s.common.KeyMap.Copy,
100 },
101 {
102 s.code.KeyMap.Down,
103 s.code.KeyMap.Up,
104 s.common.KeyMap.GotoTop,
105 s.common.KeyMap.GotoBottom,
106 },
107 }
108 return b
109}
110
111// StatusBarValue implements common.Component.
112func (s *Stash) StatusBarValue() string {
113 item, ok := s.list.SelectedItem().(StashItem)
114 if !ok {
115 return " "
116 }
117 idx := s.list.Index()
118 return fmt.Sprintf("stash@{%d}: %s", idx, item.Title())
119}
120
121// StatusBarInfo implements common.Component.
122func (s *Stash) StatusBarInfo() string {
123 switch s.state {
124 case stashStateList:
125 totalPages := s.list.TotalPages()
126 if totalPages <= 1 {
127 return "p. 1/1"
128 }
129 return fmt.Sprintf("p. %d/%d", s.list.Page()+1, totalPages)
130 case stashStatePatch:
131 return common.ScrollPercent(s.code.ScrollPosition())
132 case stashStateLoading:
133 return "Loading..."
134 default:
135 return ""
136 }
137}
138
139// SpinnerID implements common.Component.
140func (s *Stash) SpinnerID() int {
141 return s.spinner.ID()
142}
143
144// Init initializes the model.
145func (s *Stash) Init() tea.Cmd {
146 s.state = stashStateLoading
147 return tea.Batch(s.spinner.Tick, s.fetchStash)
148}
149
150// Update updates the model.
151func (s *Stash) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
152 cmds := make([]tea.Cmd, 0)
153 switch msg := msg.(type) {
154 case RepoMsg:
155 s.repo = msg
156 case RefMsg:
157 s.ref = msg
158 s.list.Select(0)
159 cmds = append(cmds, s.Init())
160 case tea.WindowSizeMsg:
161 s.SetSize(msg.Width, msg.Height)
162 case spinner.TickMsg:
163 if s.state == stashStateLoading && s.spinner.ID() == msg.ID {
164 sp, cmd := s.spinner.Update(msg)
165 s.spinner = sp
166 if cmd != nil {
167 cmds = append(cmds, cmd)
168 }
169 }
170 case tea.KeyPressMsg:
171 switch s.state {
172 case stashStateList:
173 switch {
174 case key.Matches(msg, s.common.KeyMap.BackItem):
175 cmds = append(cmds, goBackCmd)
176 case key.Matches(msg, s.common.KeyMap.Copy):
177 cmds = append(cmds, copyCmd(s.list.SelectedItem().(StashItem).Title(), "Stash message copied to clipboard"))
178 }
179 case stashStatePatch:
180 switch {
181 case key.Matches(msg, s.common.KeyMap.BackItem):
182 cmds = append(cmds, goBackCmd)
183 case key.Matches(msg, s.common.KeyMap.Copy):
184 if s.currentPatch.Diff != nil {
185 patch := s.currentPatch.Diff
186 cmds = append(cmds, copyCmd(patch.Patch(), "Stash patch copied to clipboard"))
187 }
188 }
189 case stashStateLoading:
190 // No key handling while loading
191 }
192 case StashListMsg:
193 s.state = stashStateList
194 items := make([]selector.IdentifiableItem, len(msg))
195 for i, stash := range msg {
196 items[i] = StashItem{stash}
197 }
198 cmds = append(cmds, s.list.SetItems(items))
199 case StashPatchMsg:
200 s.state = stashStatePatch
201 s.currentPatch = msg
202 if msg.Diff != nil {
203 title := s.common.Styles.Stash.Title.Render(s.list.SelectedItem().(StashItem).Title())
204 content := lipgloss.JoinVertical(lipgloss.Left,
205 title,
206 "",
207 renderSummary(msg.Diff, s.common.Styles, s.common.Width),
208 renderDiff(msg.Diff, s.common.Width),
209 )
210 cmds = append(cmds, s.code.SetContent(content, ".diff"))
211 s.code.GotoTop()
212 }
213 case selector.SelectMsg:
214 switch msg.IdentifiableItem.(type) {
215 case StashItem:
216 cmds = append(cmds, s.fetchStashPatch)
217 }
218 case GoBackMsg:
219 if s.state == stashStateList {
220 s.list.Select(0)
221 }
222 s.state = stashStateList
223 }
224 switch s.state {
225 case stashStateList:
226 l, cmd := s.list.Update(msg)
227 s.list = l.(*selector.Selector)
228 if cmd != nil {
229 cmds = append(cmds, cmd)
230 }
231 case stashStatePatch:
232 c, cmd := s.code.Update(msg)
233 s.code = c.(*code.Code)
234 if cmd != nil {
235 cmds = append(cmds, cmd)
236 }
237 case stashStateLoading:
238 // No updates while loading
239 }
240 return s, tea.Batch(cmds...)
241}
242
243// View returns the view.
244func (s *Stash) View() string {
245 switch s.state {
246 case stashStateLoading:
247 return renderLoading(s.common, s.spinner)
248 case stashStateList:
249 return s.list.View()
250 case stashStatePatch:
251 return s.code.View()
252 }
253 return ""
254}
255
256func (s *Stash) fetchStash() tea.Msg {
257 if s.repo == nil {
258 return StashListMsg(nil)
259 }
260
261 r, err := s.repo.Open()
262 if err != nil {
263 return common.ErrorMsg(err)
264 }
265
266 stash, err := r.StashList()
267 if err != nil {
268 return common.ErrorMsg(err)
269 }
270
271 return StashListMsg(stash)
272}
273
274func (s *Stash) fetchStashPatch() tea.Msg {
275 if s.repo == nil {
276 return StashPatchMsg{nil}
277 }
278
279 r, err := s.repo.Open()
280 if err != nil {
281 return common.ErrorMsg(err)
282 }
283
284 diff, err := r.StashDiff(s.list.Index())
285 if err != nil {
286 return common.ErrorMsg(err)
287 }
288
289 return StashPatchMsg{diff}
290}