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 "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 { //nolint:exhaustive
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 default:
133 return ""
134 }
135}
136
137// SpinnerID implements common.Component.
138func (s *Stash) SpinnerID() int {
139 return s.spinner.ID()
140}
141
142// Init initializes the model.
143func (s *Stash) Init() tea.Cmd {
144 s.state = stashStateLoading
145 return tea.Batch(s.spinner.Tick, s.fetchStash)
146}
147
148// Update updates the model.
149func (s *Stash) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
150 cmds := make([]tea.Cmd, 0)
151 switch msg := msg.(type) {
152 case RepoMsg:
153 s.repo = msg
154 case RefMsg:
155 s.ref = msg
156 s.list.Select(0)
157 cmds = append(cmds, s.Init())
158 case tea.WindowSizeMsg:
159 s.SetSize(msg.Width, msg.Height)
160 case spinner.TickMsg:
161 if s.state == stashStateLoading && s.spinner.ID() == msg.ID {
162 sp, cmd := s.spinner.Update(msg)
163 s.spinner = sp
164 if cmd != nil {
165 cmds = append(cmds, cmd)
166 }
167 }
168 case tea.KeyPressMsg:
169 switch s.state { //nolint:exhaustive
170 case stashStateList:
171 switch {
172 case key.Matches(msg, s.common.KeyMap.BackItem):
173 cmds = append(cmds, goBackCmd)
174 case key.Matches(msg, s.common.KeyMap.Copy):
175 cmds = append(cmds, copyCmd(s.list.SelectedItem().(StashItem).Title(), "Stash message copied to clipboard"))
176 }
177 case stashStatePatch:
178 switch {
179 case key.Matches(msg, s.common.KeyMap.BackItem):
180 cmds = append(cmds, goBackCmd)
181 case key.Matches(msg, s.common.KeyMap.Copy):
182 if s.currentPatch.Diff != nil {
183 patch := s.currentPatch.Diff
184 cmds = append(cmds, copyCmd(patch.Patch(), "Stash patch copied to clipboard"))
185 }
186 }
187 }
188 case StashListMsg:
189 s.state = stashStateList
190 items := make([]selector.IdentifiableItem, len(msg))
191 for i, stash := range msg {
192 items[i] = StashItem{stash}
193 }
194 cmds = append(cmds, s.list.SetItems(items))
195 case StashPatchMsg:
196 s.state = stashStatePatch
197 s.currentPatch = msg
198 if msg.Diff != nil {
199 title := s.common.Styles.Stash.Title.Render(s.list.SelectedItem().(StashItem).Title())
200 content := lipgloss.JoinVertical(lipgloss.Left,
201 title,
202 "",
203 renderSummary(msg.Diff, s.common.Styles, s.common.Width),
204 renderDiff(msg.Diff, s.common.Width),
205 )
206 cmds = append(cmds, s.code.SetContent(content, ".diff"))
207 s.code.GotoTop()
208 }
209 case selector.SelectMsg:
210 switch msg.IdentifiableItem.(type) {
211 case StashItem:
212 cmds = append(cmds, s.fetchStashPatch)
213 }
214 case GoBackMsg:
215 if s.state == stashStateList {
216 s.list.Select(0)
217 }
218 s.state = stashStateList
219 }
220 switch s.state { //nolint:exhaustive
221 case stashStateList:
222 l, cmd := s.list.Update(msg)
223 s.list = l.(*selector.Selector)
224 if cmd != nil {
225 cmds = append(cmds, cmd)
226 }
227 case stashStatePatch:
228 c, cmd := s.code.Update(msg)
229 s.code = c.(*code.Code)
230 if cmd != nil {
231 cmds = append(cmds, cmd)
232 }
233 }
234 return s, tea.Batch(cmds...)
235}
236
237// View returns the view.
238func (s *Stash) View() string {
239 switch s.state {
240 case stashStateLoading:
241 return renderLoading(s.common, s.spinner)
242 case stashStateList:
243 return s.list.View()
244 case stashStatePatch:
245 return s.code.View()
246 }
247 return ""
248}
249
250func (s *Stash) fetchStash() tea.Msg {
251 if s.repo == nil {
252 return StashListMsg(nil)
253 }
254
255 r, err := s.repo.Open()
256 if err != nil {
257 return common.ErrorMsg(err)
258 }
259
260 stash, err := r.StashList()
261 if err != nil {
262 return common.ErrorMsg(err)
263 }
264
265 return StashListMsg(stash)
266}
267
268func (s *Stash) fetchStashPatch() tea.Msg {
269 if s.repo == nil {
270 return StashPatchMsg{nil}
271 }
272
273 r, err := s.repo.Open()
274 if err != nil {
275 return common.ErrorMsg(err)
276 }
277
278 diff, err := r.StashDiff(s.list.Index())
279 if err != nil {
280 return common.ErrorMsg(err)
281 }
282
283 return StashPatchMsg{diff}
284}