Use config.json in config repo for menu and settings

Toby Padilla created

Change summary

git/git.go                      | 20 +++++++
main.go                         |  3 
tui/bubble.go                   | 75 +++++++++++++++++++-------
tui/bubbles/selection/bubble.go |  1 
tui/commands.go                 | 97 ++++++----------------------------
tui/defaults.go                 | 74 ++++++++++++++++++++++++++
6 files changed, 169 insertions(+), 101 deletions(-)

Detailed changes

git/git.go 🔗

@@ -142,3 +142,23 @@ func (rs *RepoSource) loadRepo(name string, rg *git.Repository) (*Repo, error) {
 	sort.Sort(rs.commits)
 	return r, nil
 }
+
+func (r *Repo) LatestFile(path string) (string, error) {
+	lg, err := r.Repository.Log(&git.LogOptions{})
+	if err != nil {
+		return "", err
+	}
+	c, err := lg.Next()
+	if err != nil {
+		return "", err
+	}
+	f, err := c.File(path)
+	if err != nil {
+		return "", nil
+	}
+	content, err := f.Contents()
+	if err != nil {
+		return "", nil
+	}
+	return content, nil
+}

main.go 🔗

@@ -7,6 +7,7 @@ import (
 	gm "smoothie/server/middleware/git"
 	lm "smoothie/server/middleware/logging"
 	"smoothie/tui"
+	"time"
 
 	"github.com/meowgorithm/babyenv"
 )
@@ -27,7 +28,7 @@ func main() {
 	s, err := server.NewServer(
 		cfg.Port,
 		cfg.KeyPath,
-		bm.Middleware(tui.SessionHandler(cfg.RepoPath)),
+		bm.Middleware(tui.SessionHandler(cfg.RepoPath, time.Second*5)),
 		gm.Middleware(cfg.RepoPath, cfg.RepoAuthPath),
 		lm.Middleware(),
 	)

tui/bubble.go 🔗

@@ -1,11 +1,13 @@
 package tui
 
 import (
+	"encoding/json"
 	"fmt"
 	"log"
 	"smoothie/git"
 	"smoothie/tui/bubbles/commits"
 	"smoothie/tui/bubbles/selection"
+	"time"
 
 	"github.com/charmbracelet/bubbles/viewport"
 	tea "github.com/charmbracelet/bubbletea"
@@ -24,14 +26,33 @@ const (
 	quitState
 )
 
+type MenuEntry struct {
+	Name string `json:"name"`
+	Repo string `json:"repo"`
+}
+
+type Config struct {
+	Name         string      `json:"name"`
+	ShowAllRepos bool        `json:"show_all_repos"`
+	Menu         []MenuEntry `json:"menu"`
+	RepoSource   *git.RepoSource
+}
+
+type SessionConfig struct {
+	Width         int
+	Height        int
+	WindowChanges <-chan ssh.Window
+}
+
 type Bubble struct {
+	config         *Config
 	state          sessionState
 	error          string
 	width          int
 	height         int
-	session        ssh.Session
 	windowChanges  <-chan ssh.Window
 	repoSource     *git.RepoSource
+	repoMenu       []MenuEntry
 	repos          []*git.Repo
 	boxes          []tea.Model
 	activeBox      int
@@ -40,25 +61,18 @@ type Bubble struct {
 	readmeViewport *ViewportBubble
 }
 
-type Config struct {
-	Width         int
-	Height        int
-	Session       ssh.Session
-	WindowChanges <-chan ssh.Window
-	RepoSource    *git.RepoSource
-}
-
-func NewBubble(cfg Config) *Bubble {
+func NewBubble(cfg *Config, sCfg *SessionConfig) *Bubble {
 	b := &Bubble{
-		width:         cfg.Width,
-		height:        cfg.Height,
-		windowChanges: cfg.WindowChanges,
+		config:        cfg,
+		width:         sCfg.Width,
+		height:        sCfg.Height,
+		windowChanges: sCfg.WindowChanges,
 		repoSource:    cfg.RepoSource,
 		boxes:         make([]tea.Model, 2),
 		readmeViewport: &ViewportBubble{
 			Viewport: &viewport.Model{
 				Width:  boxRightWidth - horizontalPadding - 2,
-				Height: cfg.Height - verticalPadding - viewportHeightConstant,
+				Height: sCfg.Height - verticalPadding - viewportHeightConstant,
 			},
 		},
 	}
@@ -91,7 +105,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		b.width = msg.Width
 		b.height = msg.Height
 	case selection.SelectedMsg:
-		cmds = append(cmds, b.getRepoCmd(b.repos[msg.Index].Name))
+		cmds = append(cmds, b.getRepoCmd(b.repoMenu[msg.Index].Repo))
 	}
 	if b.state == loadedState {
 		ab, cmd := b.boxes[b.activeBox].Update(msg)
@@ -114,7 +128,7 @@ func (b *Bubble) viewForBox(i int, width int) string {
 }
 
 func (b *Bubble) View() string {
-	h := headerStyle.Width(b.width - horizontalPadding).Render("Charm Beta")
+	h := headerStyle.Width(b.width - horizontalPadding).Render(b.config.Name)
 	f := footerStyle.Render("")
 	s := ""
 	content := ""
@@ -147,22 +161,41 @@ func glamourReadme(md string) string {
 	return mdt
 }
 
-func SessionHandler(reposPath string) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
+func SessionHandler(reposPath string, repoPoll time.Duration) func(ssh.Session) (tea.Model, []tea.ProgramOption) {
+	appCfg := &Config{}
 	rs := git.NewRepoSource(reposPath, glamourReadme)
+	appCfg.RepoSource = rs
+	go func() {
+		for {
+			_ = rs.LoadRepos()
+			cr, err := rs.GetRepo("config")
+			if err != nil {
+				log.Fatalf("cannot load config repo: %s", err)
+			}
+			cs, err := cr.LatestFile("config.json")
+			err = json.Unmarshal([]byte(cs), appCfg)
+			time.Sleep(repoPoll)
+		}
+	}()
+	err := createDefaultConfigRepo(rs)
+	if err != nil {
+		if err != nil {
+			log.Fatalf("cannot create config repo: %s", err)
+		}
+	}
+
 	return func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
 		if len(s.Command()) == 0 {
 			pty, changes, active := s.Pty()
 			if !active {
 				return nil, nil
 			}
-			cfg := Config{
+			cfg := &SessionConfig{
 				Width:         pty.Window.Width,
 				Height:        pty.Window.Height,
 				WindowChanges: changes,
-				RepoSource:    rs,
-				Session:       s,
 			}
-			return NewBubble(cfg), []tea.ProgramOption{tea.WithAltScreen()}
+			return NewBubble(appCfg, cfg), []tea.ProgramOption{tea.WithAltScreen()}
 		}
 		return nil, nil
 	}

tui/bubbles/selection/bubble.go 🔗

@@ -22,7 +22,6 @@ func NewBubble(items []string) *Bubble {
 		NormalStyle:   normalStyle,
 		SelectedStyle: selectedStyle,
 		Items:         items,
-		selectedItem:  -1,
 	}
 }
 

tui/commands.go 🔗

@@ -1,15 +1,10 @@
 package tui
 
 import (
-	"os"
-	"path/filepath"
-	"smoothie/git"
 	"smoothie/tui/bubbles/commits"
 	"smoothie/tui/bubbles/selection"
 
 	tea "github.com/charmbracelet/bubbletea"
-	gg "github.com/go-git/go-git/v5"
-	"github.com/go-git/go-git/v5/plumbing/object"
 )
 
 type windowMsg struct{}
@@ -27,78 +22,26 @@ func (b *Bubble) windowChangesCmd() tea.Msg {
 }
 
 func (b *Bubble) loadGitCmd() tea.Msg {
-	cn := "config"
-	err := b.repoSource.LoadRepos()
-	cr, err := b.repoSource.GetRepo(cn)
-	if err == git.ErrMissingRepo {
-		cr, err = b.repoSource.InitRepo(cn, false)
-		if err != nil {
-			return errMsg{err}
-		}
-
-		// Add default README and config
-		rp := filepath.Join(b.repoSource.Path, cn, "README.md")
-		rf, err := os.Create(rp)
-		if err != nil {
-			return errMsg{err}
-		}
-		defer rf.Close()
-		_, err = rf.WriteString(defaultReadme)
-		if err != nil {
-			return errMsg{err}
-		}
-		err = rf.Sync()
-		if err != nil {
-			return errMsg{err}
-		}
-		cp := filepath.Join(b.repoSource.Path, cn, "config.json")
-		cf, err := os.Create(cp)
-		if err != nil {
-			return errMsg{err}
-		}
-		defer cf.Close()
-		_, err = cf.WriteString(defaultConfig)
-		if err != nil {
-			return errMsg{err}
-		}
-		err = cf.Sync()
-		if err != nil {
-			return errMsg{err}
-		}
-		wt, err := cr.Repository.Worktree()
-		if err != nil {
-			return errMsg{err}
-		}
-		_, err = wt.Add("README.md")
-		if err != nil {
-			return errMsg{err}
-		}
-		_, err = wt.Add("config.json")
-		if err != nil {
-			return errMsg{err}
-		}
-		_, err = wt.Commit("Default init", &gg.CommitOptions{
-			All: true,
-			Author: &object.Signature{
-				Name:  "Smoothie Server",
-				Email: "vt100@charm.sh",
-			},
-		})
-		if err != nil {
-			return errMsg{err}
-		}
-		err = b.repoSource.LoadRepos()
-		if err != nil {
-			return errMsg{err}
-		}
-	} else if err != nil {
-		return errMsg{err}
-	}
 	b.repos = b.repoSource.AllRepos()
-
+	mes := make([]MenuEntry, 0)
 	rs := make([]string, 0)
-	for _, r := range b.repos {
-		rs = append(rs, r.Name)
+	for _, me := range b.config.Menu {
+		mes = append(mes, me)
+	}
+	if b.config.ShowAllRepos {
+	OUTER:
+		for _, r := range b.repos {
+			for _, me := range mes {
+				if r.Name == me.Repo {
+					continue OUTER
+				}
+			}
+			mes = append(mes, MenuEntry{Name: r.Name, Repo: r.Name})
+		}
+	}
+	b.repoMenu = mes
+	for _, me := range mes {
+		rs = append(rs, me.Name)
 	}
 	b.repoSelect = selection.NewBubble(rs)
 	b.boxes[0] = b.repoSelect
@@ -107,10 +50,8 @@ func (b *Bubble) loadGitCmd() tea.Msg {
 		boxRightWidth-horizontalPadding-2,
 		b.repoSource.GetCommits(200),
 	)
-	b.boxes[1] = b.commitsLog
-	b.activeBox = 0
 	b.state = loadedState
-	return nil
+	return b.getRepoCmd("config")()
 }
 
 func (b *Bubble) getRepoCmd(name string) tea.Cmd {

tui/defaults.go 🔗

@@ -1,5 +1,14 @@
 package tui
 
+import (
+	"os"
+	"path/filepath"
+	"smoothie/git"
+
+	gg "github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing/object"
+)
+
 const defaultReadme = "# Smoothie\nWelcome to Smoothie. To setup your own configuration, please clone this repo."
 
 const defaultConfig = `{
@@ -13,3 +22,68 @@ const defaultConfig = `{
 		}
 	]
 }`
+
+func createFile(path string, content string) error {
+	f, err := os.Create(path)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	_, err = f.WriteString(content)
+	if err != nil {
+		return err
+	}
+	return f.Sync()
+}
+
+func createDefaultConfigRepo(rs *git.RepoSource) error {
+	cn := "config"
+	err := rs.LoadRepos()
+	cr, err := rs.GetRepo(cn)
+	if err == git.ErrMissingRepo {
+		cr, err = rs.InitRepo(cn, false)
+		if err != nil {
+			return err
+		}
+
+		rp := filepath.Join(rs.Path, cn, "README.md")
+		err = createFile(rp, defaultReadme)
+		if err != nil {
+			return err
+		}
+		cp := filepath.Join(rs.Path, cn, "config.json")
+		err = createFile(cp, defaultConfig)
+		if err != nil {
+			return err
+		}
+		wt, err := cr.Repository.Worktree()
+		if err != nil {
+			return err
+		}
+		_, err = wt.Add("README.md")
+		if err != nil {
+			return err
+		}
+		_, err = wt.Add("config.json")
+		if err != nil {
+			return err
+		}
+		_, err = wt.Commit("Default init", &gg.CommitOptions{
+			All: true,
+			Author: &object.Signature{
+				Name:  "Smoothie Server",
+				Email: "vt100@charm.sh",
+			},
+		})
+		if err != nil {
+			return err
+		}
+		err = rs.LoadRepos()
+		if err != nil {
+			return err
+		}
+	} else if err != nil {
+		return err
+	}
+	return nil
+}