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}