feat: show spinner when loading commits

Ayman Bagabas created

Change summary

tui/log/bubble.go | 132 ++++++++++++++++++++++++++----------------------
1 file changed, 72 insertions(+), 60 deletions(-)

Detailed changes

tui/log/bubble.go 🔗

@@ -26,14 +26,17 @@ var (
 	waitBeforeLoading = time.Millisecond * 300
 )
 
+type itemsMsg struct{}
+
 type commitMsg *git.Commit
 
+type countMsg int64
+
 type sessionState int
 
 const (
 	logState sessionState = iota
 	commitState
-	loadingState
 	errorState
 )
 
@@ -98,6 +101,10 @@ type Bubble struct {
 	heightMargin   int
 	error          common.ErrMsg
 	spinner        spinner.Model
+	loading        bool
+	loadingStart   time.Time
+	selectedCommit *git.Commit
+	nextPage       int
 }
 
 func NewBubble(repo common.GitRepo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
@@ -132,49 +139,45 @@ func NewBubble(repo common.GitRepo, styles *style.Styles, width, widthMargin, he
 	return b
 }
 
-func (b *Bubble) reset() tea.Cmd {
-	errMsg := func(err error) tea.Cmd {
-		return func() tea.Msg { return common.ErrMsg{Err: err} }
-	}
+func (b *Bubble) countCommits() tea.Msg {
 	if b.ref == nil {
 		ref, err := b.repo.HEAD()
 		if err != nil {
-			return errMsg(err)
+			return common.ErrMsg{Err: err}
 		}
 		b.ref = ref
 	}
 	count, err := b.repo.CountCommits(b.ref)
 	if err != nil {
-		return errMsg(err)
+		return common.ErrMsg{Err: err}
 	}
-	b.count = count
-	b.state = logState
-	b.list.Select(0)
-	cmd := b.updateItems()
-	return cmd
+	return countMsg(count)
 }
 
-func (b *Bubble) updateItems() tea.Cmd {
+func (b *Bubble) updateItems() tea.Msg {
+	if b.count == 0 {
+		b.count = int64(b.countCommits().(countMsg))
+	}
 	count := b.count
 	items := make([]list.Item, count)
-	b.list.SetItems(items)
-	page := b.list.Paginator.Page
+	page := b.nextPage
 	limit := b.list.Paginator.PerPage
 	skip := page * limit
+	// CommitsByPage pages start at 1
 	cc, err := b.repo.CommitsByPage(b.ref, page+1, limit)
 	if err != nil {
-		return func() tea.Msg { return common.ErrMsg{Err: err} }
+		return common.ErrMsg{Err: err}
 	}
 	for i, c := range cc {
 		idx := i + skip
-		if idx >= int(count) {
+		if int64(idx) >= count {
 			break
 		}
 		items[idx] = item{c}
 	}
-	cmd := b.list.SetItems(items)
+	b.list.SetItems(items)
 	b.SetSize(b.width, b.height)
-	return cmd
+	return itemsMsg{}
 }
 
 func (b *Bubble) Help() []common.HelpEntry {
@@ -203,19 +206,32 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
 	case tea.WindowSizeMsg:
 		b.SetSize(msg.Width, msg.Height)
-		cmds = append(cmds, b.updateItems())
+		cmds = append(cmds, b.updateItems)
 
 	case tea.KeyMsg:
 		switch msg.String() {
 		case "C":
-			return b, b.reset()
+			b.count = 0
+			b.loading = true
+			b.loadingStart = time.Now().Add(-waitBeforeLoading) // always show spinner
+			b.list.Select(0)
+			b.nextPage = 0
+			return b, tea.Batch(b.updateItems, b.spinner.Tick)
 		case "enter", "right", "l":
 			if b.state == logState {
-				cmds = append(cmds, b.loadCommit())
+				i := b.list.SelectedItem()
+				if i != nil {
+					c, ok := i.(item)
+					if ok {
+						b.selectedCommit = c.Commit
+					}
+				}
+				cmds = append(cmds, b.loadCommit, b.spinner.Tick)
 			}
 		case "esc", "left", "h":
 			if b.state != logState {
 				b.state = logState
+				b.selectedCommit = nil
 			}
 		}
 		switch b.state {
@@ -224,7 +240,11 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			m, cmd := b.list.Update(msg)
 			b.list = m
 			if m.Paginator.Page != curPage {
-				cmds = append(cmds, b.updateItems())
+				b.loading = true
+				b.loadingStart = time.Now()
+				b.list.Paginator.Page = curPage
+				b.nextPage = m.Paginator.Page
+				cmds = append(cmds, b.updateItems, b.spinner.Tick)
 			}
 			cmds = append(cmds, cmd)
 		case commitState:
@@ -233,23 +253,28 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			cmds = append(cmds, cmd)
 		}
 		return b, tea.Batch(cmds...)
+	case itemsMsg:
+		b.loading = false
+		b.list.Paginator.Page = b.nextPage
+		if b.state != commitState {
+			b.state = logState
+		}
+	case countMsg:
+		b.count = int64(msg)
 	case common.ErrMsg:
 		b.error = msg
 		b.state = errorState
+		b.loading = false
 		return b, nil
 	case commitMsg:
-		if b.state == loadingState {
-			cmds = append(cmds, b.spinner.Tick)
-		}
+		b.loading = false
+		b.state = commitState
 	case refs.RefMsg:
 		b.ref = msg
-		count, err := b.repo.CountCommits(msg)
-		if err != nil {
-			b.error = common.ErrMsg{Err: err}
-		}
-		b.count = count
+		b.count = 0
+		cmds = append(cmds, b.countCommits)
 	case spinner.TickMsg:
-		if b.state == loadingState {
+		if b.loading {
 			s, cmd := b.spinner.Update(msg)
 			if cmd != nil {
 				cmds = append(cmds, cmd)
@@ -299,33 +324,14 @@ func (b *Bubble) loadPatch(c *git.Commit) error {
 	return nil
 }
 
-func (b *Bubble) loadCommit() tea.Cmd {
-	var err error
-	done := make(chan struct{}, 1)
-	i := b.list.SelectedItem()
-	if i == nil {
-		return nil
-	}
-	c, ok := i.(item)
-	if !ok {
-		return nil
-	}
-	go func() {
-		err = b.loadPatch(c.Commit)
-		done <- struct{}{}
-		b.state = commitState
-	}()
-	return func() tea.Msg {
-		select {
-		case <-done:
-		case <-time.After(waitBeforeLoading):
-			b.state = loadingState
-		}
-		if err != nil {
-			return common.ErrMsg{Err: err}
-		}
-		return commitMsg(c.Commit)
+func (b *Bubble) loadCommit() tea.Msg {
+	b.loading = true
+	b.loadingStart = time.Now()
+	c := b.selectedCommit
+	if err := b.loadPatch(c); err != nil {
+		return common.ErrMsg{Err: err}
 	}
+	return commitMsg(c)
 }
 
 func (b *Bubble) renderCommit(c *git.Commit) string {
@@ -356,11 +362,17 @@ func (b *Bubble) renderDiff(diff *git.Diff) string {
 }
 
 func (b *Bubble) View() string {
+	if b.loading && b.loadingStart.Add(waitBeforeLoading).Before(time.Now()) {
+		msg := fmt.Sprintf("%s loading commit", b.spinner.View())
+		if b.selectedCommit == nil {
+			msg += "s"
+		}
+		msg += "…"
+		return msg
+	}
 	switch b.state {
 	case logState:
 		return b.list.View()
-	case loadingState:
-		return fmt.Sprintf("%s loading commit…", b.spinner.View())
 	case errorState:
 		return b.error.ViewWithPrefix(b.style, "Error")
 	case commitState: