root.go

  1package main
  2
  3import (
  4	"fmt"
  5	"log"
  6	"os"
  7	"path/filepath"
  8	"runtime/debug"
  9
 10	"github.com/aymanbagabas/go-osc52"
 11	"github.com/charmbracelet/bubbles/key"
 12	tea "github.com/charmbracelet/bubbletea"
 13	"github.com/charmbracelet/lipgloss"
 14	ggit "github.com/charmbracelet/soft-serve/git"
 15	"github.com/charmbracelet/soft-serve/ui/common"
 16	"github.com/charmbracelet/soft-serve/ui/components/footer"
 17	"github.com/charmbracelet/soft-serve/ui/git"
 18	"github.com/charmbracelet/soft-serve/ui/keymap"
 19	"github.com/charmbracelet/soft-serve/ui/pages/repo"
 20	"github.com/charmbracelet/soft-serve/ui/styles"
 21	"github.com/spf13/cobra"
 22	"golang.org/x/term"
 23)
 24
 25var (
 26	// Version contains the application version number. It's set via ldflags
 27	// when building.
 28	Version = ""
 29
 30	// CommitSHA contains the SHA of the commit that this application was built
 31	// against. It's set via ldflags when building.
 32	CommitSHA = ""
 33
 34	rootCmd = &cobra.Command{
 35		Use:   "soft",
 36		Short: "Soft Serve, a self-hostable Git server for the command line.",
 37		Long:  "Soft Serve is a self-hostable Git server for the command line.",
 38		Args:  cobra.MaximumNArgs(1),
 39		RunE: func(cmd *cobra.Command, args []string) error {
 40			path, err := os.Getwd()
 41			if err != nil {
 42				return err
 43			}
 44			if len(args) > 0 {
 45				p := args[0]
 46				if filepath.IsAbs(p) {
 47					path = p
 48				} else {
 49					path = filepath.Join(path, p)
 50				}
 51			}
 52			path = filepath.Clean(path)
 53			w, h, _ := term.GetSize(int(os.Stdout.Fd()))
 54			c := common.Common{
 55				Styles: styles.DefaultStyles(),
 56				KeyMap: keymap.DefaultKeyMap(),
 57				Copy:   osc52.NewOutput(os.Stdout, os.Environ()),
 58				Width:  w,
 59				Height: h,
 60			}
 61			repo := repo.New(nil, c)
 62			repo.BackKey.SetHelp("esc", "quit")
 63			ui := &ui{
 64				c:    c,
 65				repo: repo,
 66				path: path,
 67			}
 68			ui.footer = footer.New(c, ui)
 69			p := tea.NewProgram(ui,
 70				tea.WithMouseCellMotion(),
 71				tea.WithAltScreen(),
 72			)
 73			if len(os.Getenv("DEBUG")) > 0 {
 74				f, err := tea.LogToFile("soft.log", "")
 75				if err != nil {
 76					log.Fatal(err)
 77				}
 78				defer f.Close() // nolint: errcheck
 79			}
 80			return p.Start()
 81		},
 82	}
 83)
 84
 85type state int
 86
 87const (
 88	stateLoading state = iota
 89	stateReady
 90	stateError
 91)
 92
 93type ui struct {
 94	c      common.Common
 95	state  state
 96	repo   *repo.Repo
 97	footer *footer.Footer
 98	path   string
 99	ref    *ggit.Reference
100	r      git.GitRepo
101	error  error
102}
103
104func (u *ui) ShortHelp() []key.Binding {
105	return u.repo.ShortHelp()
106}
107
108func (u *ui) FullHelp() [][]key.Binding {
109	return u.repo.FullHelp()
110}
111
112func (u *ui) SetSize(width, height int) {
113	u.c.SetSize(width, height)
114	hm := u.c.Styles.App.GetVerticalFrameSize()
115	wm := u.c.Styles.App.GetHorizontalFrameSize()
116	if u.footer.ShowAll() {
117		hm += u.footer.Height()
118	}
119	u.footer.SetSize(width-wm, height-hm)
120	u.repo.SetSize(width-wm, height-hm)
121}
122
123func (u *ui) Init() tea.Cmd {
124	r, err := git.NewRepo(u.path)
125	if err != nil {
126		return common.ErrorCmd(err)
127	}
128	h, err := r.HEAD()
129	if err != nil {
130		return common.ErrorCmd(err)
131	}
132	u.r = r
133	u.ref = h
134	return tea.Batch(
135		func() tea.Msg {
136			return repo.RefMsg(h)
137		},
138		func() tea.Msg {
139			return repo.RepoMsg(r)
140		},
141	)
142}
143
144func (u *ui) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
145	cmds := make([]tea.Cmd, 0)
146	switch msg := msg.(type) {
147	case repo.RefMsg, repo.RepoMsg:
148		if u.ref != nil && u.r != nil {
149			u.state = stateReady
150		}
151	case tea.KeyMsg:
152		switch {
153		case key.Matches(msg, u.c.KeyMap.Help):
154			u.footer.SetShowAll(!u.footer.ShowAll())
155		case key.Matches(msg, u.c.KeyMap.Quit), key.Matches(msg, u.c.KeyMap.Back):
156			return u, tea.Quit
157		}
158	case tea.WindowSizeMsg:
159		u.SetSize(msg.Width, msg.Height)
160	case common.ErrorMsg:
161		if u.state != stateLoading {
162			u.error = msg
163			u.state = stateError
164		}
165	}
166	r, cmd := u.repo.Update(msg)
167	u.repo = r.(*repo.Repo)
168	if cmd != nil {
169		cmds = append(cmds, cmd)
170	}
171	// This fixes determining the height margin of the footer.
172	u.SetSize(u.c.Width, u.c.Height)
173	return u, tea.Batch(cmds...)
174}
175
176func (u *ui) View() string {
177	var view string
178	switch u.state {
179	case stateLoading:
180		view = "Loading..."
181	case stateReady:
182		view = u.repo.View()
183		if u.footer.ShowAll() {
184			view = lipgloss.JoinVertical(lipgloss.Top,
185				view,
186				u.footer.View(),
187			)
188		}
189	case stateError:
190		view = fmt.Sprintf("Error: %s", u.error)
191	}
192	return u.c.Styles.App.Render(view)
193}
194
195func init() {
196	if len(CommitSHA) >= 7 {
197		vt := rootCmd.VersionTemplate()
198		rootCmd.SetVersionTemplate(vt[:len(vt)-1] + " (" + CommitSHA[0:7] + ")\n")
199	}
200	if Version == "" {
201		if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
202			Version = info.Main.Version
203		} else {
204			Version = "unknown (built from source)"
205		}
206	}
207	rootCmd.Version = Version
208
209	rootCmd.AddCommand(
210		serveCmd,
211	)
212}
213
214func main() {
215	if err := rootCmd.Execute(); err != nil {
216		log.Fatal(err)
217	}
218}