stash.go

  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}