feat(ui): display quick guide for empty repositories

Ayman Bagabas created

Change summary

ui/components/statusbar/statusbar.go | 12 ++++++++
ui/pages/repo/empty.go               | 45 ++++++++++++++++++++++++++++++
ui/pages/repo/readme.go              | 20 ++++++++++---
ui/pages/repo/refs.go                |  3 ++
ui/pages/repo/repo.go                | 20 +++++++++----
ui/styles/styles.go                  |  7 ----
ui/ui.go                             | 21 ++++++-------
7 files changed, 100 insertions(+), 28 deletions(-)

Detailed changes

ui/components/statusbar/statusbar.go πŸ”—

@@ -86,3 +86,15 @@ func (s *StatusBar) View() string {
 			),
 		)
 }
+
+// StatusBarCmd returns a command that sets the status bar information.
+func StatusBarCmd(key, value, info, branch string) tea.Cmd {
+	return func() tea.Msg {
+		return StatusBarMsg{
+			Key:    key,
+			Value:  value,
+			Info:   info,
+			Branch: branch,
+		}
+	}
+}

ui/pages/repo/empty.go πŸ”—

@@ -0,0 +1,45 @@
+package repo
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/charmbracelet/soft-serve/server/config"
+)
+
+func defaultEmptyRepoMsg(cfg *config.Config, repo string) string {
+	host := cfg.Host
+	if cfg.SSH.Port != 22 {
+		host = fmt.Sprintf("%s:%d", host, cfg.SSH.Port)
+	}
+	repo = strings.TrimSuffix(repo, ".git")
+	return fmt.Sprintf(`# Quick Start
+
+Get started by cloning this repository, add your files, commit, and push.
+
+## Clone this repository.
+
+`+"```"+`sh
+git clone ssh://%[1]s/%[2]s.git
+`+"```"+`
+
+## Creating a new repository on the command line
+
+`+"```"+`sh
+touch README.md
+git init
+git add README.md
+git branch -M main
+git commit -m "first commit"
+git remote add origin ssh://%[1]s/%[2]s.git
+git push -u origin main
+`+"```"+`
+
+## Pushing an existing repository from the command line
+
+`+"```"+`sh
+git remote add origin ssh://%[1]s/%[2]s.git
+git push -u origin main
+`+"```"+`
+`, host, repo)
+}

ui/pages/repo/readme.go πŸ”—

@@ -2,6 +2,7 @@ package repo
 
 import (
 	"fmt"
+	"path/filepath"
 
 	"github.com/charmbracelet/bubbles/key"
 	tea "github.com/charmbracelet/bubbletea"
@@ -17,10 +18,11 @@ type ReadmeMsg struct {
 
 // Readme is the readme component page.
 type Readme struct {
-	common common.Common
-	code   *code.Code
-	ref    RefMsg
-	repo   *git.Repository
+	common     common.Common
+	code       *code.Code
+	ref        RefMsg
+	repo       *git.Repository
+	readmePath string
 }
 
 // NewReadme creates a new readme model.
@@ -79,6 +81,9 @@ func (r *Readme) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case RefMsg:
 		r.ref = msg
 		cmds = append(cmds, r.Init())
+	case EmptyRepoMsg:
+		r.code.SetContent(defaultEmptyRepoMsg(r.common.Config(),
+			r.repo.Info.Name()), ".md")
 	}
 	c, cmd := r.code.Update(msg)
 	r.code = c.(*code.Code)
@@ -95,7 +100,11 @@ func (r *Readme) View() string {
 
 // StatusBarValue implements statusbar.StatusBar.
 func (r *Readme) StatusBarValue() string {
-	return ""
+	dir := filepath.Dir(r.readmePath)
+	if dir == "." {
+		return ""
+	}
+	return dir
 }
 
 // StatusBarInfo implements statusbar.StatusBar.
@@ -109,6 +118,7 @@ func (r *Readme) updateReadmeCmd() tea.Msg {
 		return common.ErrorCmd(git.ErrMissingRepo)
 	}
 	rm, rp := r.repo.Readme()
+	r.readmePath = rp
 	r.code.GotoTop()
 	cmd := r.code.SetContent(rm, rp)
 	if cmd != nil {

ui/pages/repo/refs.go πŸ”—

@@ -204,6 +204,9 @@ func UpdateRefCmd(repo *git.Repository) tea.Cmd {
 	return func() tea.Msg {
 		ref, err := repo.Repo.Repository().HEAD()
 		if err != nil {
+			if bs, err := repo.Repo.Repository().Branches(); err != nil && len(bs) == 0 {
+				return EmptyRepoMsg{}
+			}
 			log.Printf("ui: error getting HEAD reference: %v", err)
 			return common.ErrorMsg(err)
 		}

ui/pages/repo/repo.go πŸ”—

@@ -21,7 +21,7 @@ type state int
 
 const (
 	loadingState state = iota
-	loadedState
+	readyState
 )
 
 type tab int
@@ -45,6 +45,9 @@ func (t tab) String() string {
 	}[t]
 }
 
+// EmptyRepoMsg is a message to indicate that the repository is empty.
+type EmptyRepoMsg struct{}
+
 // CopyURLMsg is a message to copy the URL of the current repository.
 type CopyURLMsg struct{}
 
@@ -257,6 +260,11 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		cmds = append(cmds, r.updateStatusBarCmd)
 	case tea.WindowSizeMsg:
 		cmds = append(cmds, r.updateModels(msg))
+	case EmptyRepoMsg:
+		r.state = readyState
+		cmds = append(cmds, r.updateStatusBarCmd)
+	case common.ErrorMsg:
+		r.state = readyState
 	}
 	s, cmd := r.statusbar.Update(msg)
 	r.statusbar = s.(*statusbar.StatusBar)
@@ -290,7 +298,7 @@ func (r *Repo) View() string {
 	switch r.state {
 	case loadingState:
 		main = fmt.Sprintf("%s loading…", r.spinner.View())
-	case loadedState:
+	case readyState:
 		main = r.panes[r.activeTab].View()
 		statusbar = r.statusbar.View()
 	}
@@ -353,15 +361,15 @@ func (r *Repo) updateStatusBarCmd() tea.Msg {
 	}
 	value := r.panes[r.activeTab].(statusbar.Model).StatusBarValue()
 	info := r.panes[r.activeTab].(statusbar.Model).StatusBarInfo()
-	ref := ""
+	branch := "*"
 	if r.ref != nil {
-		ref = r.ref.Name().Short()
+		branch += " " + r.ref.Name().Short()
 	}
 	return statusbar.StatusBarMsg{
 		Key:    r.selectedRepo.Info.Name(),
 		Value:  value,
 		Info:   info,
-		Branch: fmt.Sprintf("* %s", ref),
+		Branch: branch,
 	}
 }
 
@@ -418,7 +426,7 @@ func (r *Repo) updateRepo(msg tea.Msg) tea.Cmd {
 		r.panesReady[readmeTab] = true
 	}
 	if r.isReady() {
-		r.state = loadedState
+		r.state = readyState
 	}
 	return tea.Batch(cmds...)
 }

ui/styles/styles.go πŸ”—

@@ -119,7 +119,6 @@ type Styles struct {
 		Selector    lipgloss.Style
 		FileContent lipgloss.Style
 		Paginator   lipgloss.Style
-		NoItems     lipgloss.Style
 	}
 
 	Spinner          lipgloss.Style
@@ -405,8 +404,6 @@ func DefaultStyles() *Styles {
 
 	s.Tree.Paginator = s.Log.Paginator.Copy()
 
-	s.Tree.NoItems = s.AboutNoReadme.Copy()
-
 	s.Spinner = lipgloss.NewStyle().
 		MarginTop(1).
 		MarginLeft(2).
@@ -420,9 +417,7 @@ func DefaultStyles() *Styles {
 		MarginLeft(2).
 		Foreground(lipgloss.Color("242"))
 
-	s.NoItems = lipgloss.NewStyle().
-		MarginLeft(2).
-		Foreground(lipgloss.Color("242"))
+	s.NoItems = s.AboutNoReadme.Copy()
 
 	s.StatusBar = lipgloss.NewStyle().
 		Height(1)

ui/ui.go πŸ”—

@@ -27,9 +27,9 @@ const (
 type sessionState int
 
 const (
-	startState sessionState = iota
+	loadingState sessionState = iota
 	errorState
-	loadedState
+	readyState
 )
 
 // UI is the main UI model.
@@ -58,7 +58,7 @@ func New(c common.Common, initialRepo string) *UI {
 		common:      c,
 		pages:       make([]common.Component, 2), // selection & repo
 		activePage:  selectionPage,
-		state:       startState,
+		state:       loadingState,
 		header:      h,
 		initialRepo: initialRepo,
 		showFooter:  true,
@@ -92,7 +92,7 @@ func (ui *UI) ShortHelp() []key.Binding {
 	switch ui.state {
 	case errorState:
 		b = append(b, ui.common.KeyMap.Back)
-	case loadedState:
+	case readyState:
 		b = append(b, ui.pages[ui.activePage].ShortHelp()...)
 	}
 	if !ui.IsFiltering() {
@@ -108,7 +108,7 @@ func (ui *UI) FullHelp() [][]key.Binding {
 	switch ui.state {
 	case errorState:
 		b = append(b, []key.Binding{ui.common.KeyMap.Back})
-	case loadedState:
+	case readyState:
 		b = append(b, ui.pages[ui.activePage].FullHelp()...)
 	}
 	h := []key.Binding{
@@ -147,7 +147,7 @@ func (ui *UI) Init() tea.Cmd {
 	if ui.initialRepo != "" {
 		cmds = append(cmds, ui.initialRepoCmd(ui.initialRepo))
 	}
-	ui.state = loadedState
+	ui.state = readyState
 	ui.SetSize(ui.common.Width, ui.common.Height)
 	return tea.Batch(cmds...)
 }
@@ -182,7 +182,7 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			switch {
 			case key.Matches(msg, ui.common.KeyMap.Back) && ui.error != nil:
 				ui.error = nil
-				ui.state = loadedState
+				ui.state = readyState
 				// Always show the footer on error.
 				ui.showFooter = ui.footer.ShowAll()
 			case key.Matches(msg, ui.common.KeyMap.Help):
@@ -223,7 +223,6 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		ui.error = msg
 		ui.state = errorState
 		ui.showFooter = true
-		return ui, nil
 	case selector.SelectMsg:
 		switch msg.IdentifiableItem.(type) {
 		case selection.Item:
@@ -242,7 +241,7 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	if cmd != nil {
 		cmds = append(cmds, cmd)
 	}
-	if ui.state == loadedState {
+	if ui.state != loadingState {
 		m, cmd := ui.pages[ui.activePage].Update(msg)
 		ui.pages[ui.activePage] = m.(common.Component)
 		if cmd != nil {
@@ -259,7 +258,7 @@ func (ui *UI) View() string {
 	var view string
 	wm, hm := ui.getMargins()
 	switch ui.state {
-	case startState:
+	case loadingState:
 		view = "Loading..."
 	case errorState:
 		err := ui.common.Styles.ErrorTitle.Render("Bummer")
@@ -272,7 +271,7 @@ func (ui *UI) View() string {
 				hm -
 				ui.common.Styles.Error.GetVerticalFrameSize()).
 			Render(err)
-	case loadedState:
+	case readyState:
 		view = ui.pages[ui.activePage].View()
 	default:
 		view = "Unknown state :/ this is a bug!"