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	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}