stash.go

  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}