1package repo
2
3import (
4 "fmt"
5
6 "github.com/charmbracelet/bubbles/key"
7 "github.com/charmbracelet/bubbles/spinner"
8 tea "github.com/charmbracelet/bubbletea"
9 "github.com/charmbracelet/lipgloss"
10 "github.com/charmbracelet/soft-serve/git"
11 "github.com/charmbracelet/soft-serve/pkg/proto"
12 "github.com/charmbracelet/soft-serve/pkg/ui/common"
13 "github.com/charmbracelet/soft-serve/pkg/ui/components/code"
14 "github.com/charmbracelet/soft-serve/pkg/ui/components/selector"
15 gitm "github.com/aymanbagabas/git-module"
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// TabName returns the name of the tab.
68func (s *Stash) TabName() string {
69 return "Stash"
70}
71
72// SetSize implements common.Component.
73func (s *Stash) SetSize(width, height int) {
74 s.common.SetSize(width, height)
75 s.code.SetSize(width, height)
76 s.list.SetSize(width, height)
77}
78
79// ShortHelp implements help.KeyMap.
80func (s *Stash) ShortHelp() []key.Binding {
81 return []key.Binding{
82 s.common.KeyMap.Select,
83 s.common.KeyMap.Back,
84 s.common.KeyMap.UpDown,
85 }
86}
87
88// FullHelp implements help.KeyMap.
89func (s *Stash) FullHelp() [][]key.Binding {
90 b := [][]key.Binding{
91 {
92 s.common.KeyMap.Select,
93 s.common.KeyMap.Back,
94 s.common.KeyMap.Copy,
95 },
96 {
97 s.code.KeyMap.Down,
98 s.code.KeyMap.Up,
99 s.common.KeyMap.GotoTop,
100 s.common.KeyMap.GotoBottom,
101 },
102 }
103 return b
104}
105
106// StatusBarValue implements common.Component.
107func (s *Stash) StatusBarValue() string {
108 item, ok := s.list.SelectedItem().(StashItem)
109 if !ok {
110 return " "
111 }
112 idx := s.list.Index()
113 return fmt.Sprintf("stash@{%d}: %s", idx, item.Title())
114}
115
116// StatusBarInfo implements common.Component.
117func (s *Stash) StatusBarInfo() string {
118 switch s.state {
119 case stashStateList:
120 totalPages := s.list.TotalPages()
121 if totalPages <= 1 {
122 return "p. 1/1"
123 }
124 return fmt.Sprintf("p. %d/%d", s.list.Page()+1, totalPages)
125 case stashStatePatch:
126 return fmt.Sprintf("☰ %d%%", s.code.ScrollPosition())
127 default:
128 return ""
129 }
130}
131
132// SpinnerID implements common.Component.
133func (s *Stash) SpinnerID() int {
134 return s.spinner.ID()
135}
136
137// Init initializes the model.
138func (s *Stash) Init() tea.Cmd {
139 s.state = stashStateLoading
140 return tea.Batch(s.spinner.Tick, s.fetchStash)
141}
142
143// Update updates the model.
144func (s *Stash) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
145 cmds := make([]tea.Cmd, 0)
146 switch msg := msg.(type) {
147 case RepoMsg:
148 s.repo = msg
149 case RefMsg:
150 s.ref = msg
151 s.list.Select(0)
152 cmds = append(cmds, s.Init())
153 case tea.WindowSizeMsg:
154 s.SetSize(msg.Width, msg.Height)
155 case spinner.TickMsg:
156 if s.state == stashStateLoading && s.spinner.ID() == msg.ID {
157 sp, cmd := s.spinner.Update(msg)
158 s.spinner = sp
159 if cmd != nil {
160 cmds = append(cmds, cmd)
161 }
162 }
163 case tea.KeyMsg:
164 switch s.state {
165 case stashStateList:
166 switch {
167 case key.Matches(msg, s.common.KeyMap.BackItem):
168 cmds = append(cmds, goBackCmd)
169 case key.Matches(msg, s.common.KeyMap.Copy):
170 cmds = append(cmds, copyCmd(s.list.SelectedItem().(StashItem).Title(), "Stash message copied to clipboard"))
171 }
172 case stashStatePatch:
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 if s.currentPatch.Diff != nil {
178 patch := s.currentPatch.Diff
179 cmds = append(cmds, copyCmd(patch.Patch(), "Stash patch copied to clipboard"))
180 }
181 }
182 }
183 case StashListMsg:
184 s.state = stashStateList
185 items := make([]selector.IdentifiableItem, len(msg))
186 for i, stash := range msg {
187 items[i] = StashItem{stash}
188 }
189 cmds = append(cmds, s.list.SetItems(items))
190 case StashPatchMsg:
191 s.state = stashStatePatch
192 s.currentPatch = msg
193 if msg.Diff != nil {
194 title := s.common.Styles.Stash.Title.Render(s.list.SelectedItem().(StashItem).Title())
195 content := lipgloss.JoinVertical(lipgloss.Top,
196 title,
197 "",
198 renderSummary(msg.Diff, s.common.Styles, s.common.Width),
199 renderDiff(msg.Diff, s.common.Width),
200 )
201 cmds = append(cmds, s.code.SetContent(content, ".diff"))
202 s.code.GotoTop()
203 }
204 case selector.SelectMsg:
205 switch msg.IdentifiableItem.(type) {
206 case StashItem:
207 cmds = append(cmds, s.fetchStashPatch)
208 }
209 case GoBackMsg:
210 if s.state == stashStateList {
211 s.list.Select(0)
212 }
213 s.state = stashStateList
214 }
215 switch s.state {
216 case stashStateList:
217 l, cmd := s.list.Update(msg)
218 s.list = l.(*selector.Selector)
219 if cmd != nil {
220 cmds = append(cmds, cmd)
221 }
222 case stashStatePatch:
223 c, cmd := s.code.Update(msg)
224 s.code = c.(*code.Code)
225 if cmd != nil {
226 cmds = append(cmds, cmd)
227 }
228 }
229 return s, tea.Batch(cmds...)
230}
231
232// View returns the view.
233func (s *Stash) View() string {
234 switch s.state {
235 case stashStateLoading:
236 return renderLoading(s.common, s.spinner)
237 case stashStateList:
238 return s.list.View()
239 case stashStatePatch:
240 return s.code.View()
241 }
242 return ""
243}
244
245func (s *Stash) fetchStash() tea.Msg {
246 if s.repo == nil {
247 return StashListMsg(nil)
248 }
249
250 r, err := s.repo.Open()
251 if err != nil {
252 return common.ErrorMsg(err)
253 }
254
255 stash, err := r.StashList()
256 if err != nil {
257 return common.ErrorMsg(err)
258 }
259
260 return StashListMsg(stash)
261}
262
263func (s *Stash) fetchStashPatch() tea.Msg {
264 if s.repo == nil {
265 return StashPatchMsg{nil}
266 }
267
268 r, err := s.repo.Open()
269 if err != nil {
270 return common.ErrorMsg(err)
271 }
272
273 diff, err := r.StashDiff(s.list.Index())
274 if err != nil {
275 return common.ErrorMsg(err)
276 }
277
278 return StashPatchMsg{diff}
279}