feat: add log commits loading

Ayman Bagabas created

Change summary

ui/pages/repo/log.go  | 64 ++++++++++++++++++++++++++++++++++++++++++--
ui/pages/repo/repo.go |  6 +++
2 files changed, 65 insertions(+), 5 deletions(-)

Detailed changes

ui/pages/repo/log.go 🔗

@@ -6,6 +6,7 @@ import (
 	"time"
 
 	"github.com/charmbracelet/bubbles/key"
+	"github.com/charmbracelet/bubbles/spinner"
 	tea "github.com/charmbracelet/bubbletea"
 	gansi "github.com/charmbracelet/glamour/ansi"
 	"github.com/charmbracelet/lipgloss"
@@ -18,6 +19,10 @@ import (
 	"github.com/muesli/termenv"
 )
 
+var (
+	waitBeforeLoading = time.Millisecond * 200
+)
+
 type logView int
 
 const (
@@ -50,6 +55,9 @@ type Log struct {
 	activeCommit   *ggit.Commit
 	selectedCommit *ggit.Commit
 	currentDiff    *ggit.Diff
+	loadingTime    time.Time
+	loading        bool
+	spinner        spinner.Model
 }
 
 // NewLog creates a new Log model.
@@ -70,6 +78,10 @@ func NewLog(common common.Common) *Log {
 	selector.KeyMap.NextPage = common.KeyMap.NextPage
 	selector.KeyMap.PrevPage = common.KeyMap.PrevPage
 	l.selector = selector
+	s := spinner.New()
+	s.Spinner = spinner.Dot
+	s.Style = common.Styles.Spinner
+	l.spinner = s
 	return l
 }
 
@@ -147,6 +159,16 @@ func (l *Log) FullHelp() [][]key.Binding {
 	return b
 }
 
+func (l *Log) startLoading() tea.Msg {
+	l.loadingTime = time.Now()
+	l.loading = true
+	return l.spinner.Tick()
+}
+
+func (l *Log) stopLoading() {
+	l.loading = false
+}
+
 // Init implements tea.Model.
 func (l *Log) Init() tea.Cmd {
 	l.activeView = logViewCommits
@@ -154,7 +176,11 @@ func (l *Log) Init() tea.Cmd {
 	l.count = 0
 	l.activeCommit = nil
 	l.selector.Select(0)
-	return l.updateCommitsCmd
+	return tea.Batch(
+		l.updateCommitsCmd,
+		// start loading on init
+		l.startLoading,
+	)
 }
 
 // Update implements tea.Model.
@@ -177,6 +203,8 @@ func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		if i != nil {
 			l.activeCommit = i.(LogItem).Commit
 		}
+		// stop loading after receiving items
+		l.stopLoading()
 	case tea.KeyMsg, tea.MouseMsg:
 		switch l.activeView {
 		case logViewCommits:
@@ -195,7 +223,10 @@ func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			if m.Page() != curPage {
 				l.nextPage = m.Page()
 				l.selector.SetPage(curPage)
-				cmds = append(cmds, l.updateCommitsCmd)
+				cmds = append(cmds,
+					l.updateCommitsCmd,
+					l.startLoading,
+				)
 			}
 			cmds = append(cmds, cmd)
 		case logViewDiff:
@@ -216,7 +247,10 @@ func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case selector.SelectMsg:
 		switch sel := msg.IdentifiableItem.(type) {
 		case LogItem:
-			cmds = append(cmds, l.selectCommitCmd(sel.Commit))
+			cmds = append(cmds,
+				l.selectCommitCmd(sel.Commit),
+				l.startLoading,
+			)
 		}
 	case LogCommitMsg:
 		l.selectedCommit = msg
@@ -233,6 +267,8 @@ func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		l.vp.GotoTop()
 		l.activeView = logViewDiff
 		cmds = append(cmds, updateStatusBarCmd)
+		// stop loading after setting the viewport content
+		l.stopLoading()
 	case tea.WindowSizeMsg:
 		if l.selectedCommit != nil && l.currentDiff != nil {
 			l.vp.SetContent(
@@ -244,9 +280,21 @@ func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			)
 		}
 		if l.repo != nil {
-			cmds = append(cmds, l.updateCommitsCmd)
+			cmds = append(cmds,
+				l.updateCommitsCmd,
+				// start loading on resize since the number of commits per page
+				// might change and we'd need to load more commits.
+				l.startLoading,
+			)
 		}
 	}
+	if l.loading {
+		s, cmd := l.spinner.Update(msg)
+		if cmd != nil {
+			cmds = append(cmds, cmd)
+		}
+		l.spinner = s
+	}
 	switch l.activeView {
 	case logViewDiff:
 		vp, cmd := l.vp.Update(msg)
@@ -260,6 +308,14 @@ func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 // View implements tea.Model.
 func (l *Log) View() string {
+	if l.loading && l.loadingTime.Add(waitBeforeLoading).Before(time.Now()) {
+		msg := fmt.Sprintf("%s loading commit", l.spinner.View())
+		if l.selectedCommit == nil {
+			msg += "s"
+		}
+		msg += "…"
+		return msg
+	}
 	switch l.activeView {
 	case logViewCommits:
 		return l.selector.View()

ui/pages/repo/repo.go 🔗

@@ -5,6 +5,7 @@ import (
 
 	"github.com/charmbracelet/bubbles/help"
 	"github.com/charmbracelet/bubbles/key"
+	"github.com/charmbracelet/bubbles/spinner"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
 	"github.com/charmbracelet/soft-serve/config"
@@ -174,7 +175,10 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		if cmd != nil {
 			cmds = append(cmds, cmd)
 		}
-	case LogCountMsg, LogItemsMsg:
+	// The Log bubble is the only bubble that uses a spinner, so this is fine
+	// for now. We need to pass the TickMsg to the Log bubble when the Log is
+	// loading but not the current selected tab so that the spinner works.
+	case LogCountMsg, LogItemsMsg, spinner.TickMsg:
 		l, cmd := r.boxes[commitsTab].Update(msg)
 		r.boxes[commitsTab] = l.(*Log)
 		if cmd != nil {