ui.go

  1package ui
  2
  3import (
  4	"log"
  5
  6	"github.com/charmbracelet/bubbles/key"
  7	tea "github.com/charmbracelet/bubbletea"
  8	"github.com/charmbracelet/lipgloss"
  9	"github.com/charmbracelet/soft-serve/ui/common"
 10	"github.com/charmbracelet/soft-serve/ui/components/footer"
 11	"github.com/charmbracelet/soft-serve/ui/components/header"
 12	"github.com/charmbracelet/soft-serve/ui/components/selector"
 13	"github.com/charmbracelet/soft-serve/ui/git"
 14	"github.com/charmbracelet/soft-serve/ui/pages/repo"
 15	"github.com/charmbracelet/soft-serve/ui/pages/selection"
 16	"github.com/charmbracelet/soft-serve/ui/session"
 17)
 18
 19type page int
 20
 21const (
 22	selectionPage page = iota
 23	repoPage
 24)
 25
 26type sessionState int
 27
 28const (
 29	startState sessionState = iota
 30	errorState
 31	loadedState
 32)
 33
 34// UI is the main UI model.
 35type UI struct {
 36	s           session.Session
 37	initialRepo string
 38	common      common.Common
 39	pages       []common.Page
 40	activePage  page
 41	state       sessionState
 42	header      *header.Header
 43	footer      *footer.Footer
 44	error       error
 45}
 46
 47// New returns a new UI model.
 48func New(s session.Session, c common.Common, initialRepo string) *UI {
 49	h := header.New(c, s.Config().Name)
 50	ui := &UI{
 51		s:           s,
 52		common:      c,
 53		pages:       make([]common.Page, 2), // selection & repo
 54		activePage:  selectionPage,
 55		state:       startState,
 56		header:      h,
 57		initialRepo: initialRepo,
 58	}
 59	ui.footer = footer.New(c, ui)
 60	return ui
 61}
 62
 63func (ui *UI) getMargins() (wm, hm int) {
 64	wm = ui.common.Styles.App.GetHorizontalFrameSize()
 65	hm = ui.common.Styles.App.GetVerticalFrameSize() +
 66		ui.common.Styles.Footer.GetVerticalFrameSize() +
 67		ui.footer.Height()
 68	switch ui.activePage {
 69	case selectionPage:
 70		hm += ui.common.Styles.Header.GetHeight() +
 71			ui.common.Styles.Header.GetVerticalFrameSize()
 72	case repoPage:
 73	}
 74	return
 75}
 76
 77// ShortHelp implements help.KeyMap.
 78func (ui *UI) ShortHelp() []key.Binding {
 79	b := make([]key.Binding, 0)
 80	switch ui.state {
 81	case errorState:
 82		b = append(b, ui.common.KeyMap.Back)
 83	case loadedState:
 84		b = append(b, ui.pages[ui.activePage].ShortHelp()...)
 85	}
 86	b = append(b,
 87		ui.common.KeyMap.Quit,
 88		ui.common.KeyMap.Help,
 89	)
 90	return b
 91}
 92
 93// FullHelp implements help.KeyMap.
 94func (ui *UI) FullHelp() [][]key.Binding {
 95	b := make([][]key.Binding, 0)
 96	switch ui.state {
 97	case errorState:
 98		b = append(b, []key.Binding{ui.common.KeyMap.Back})
 99	case loadedState:
100		b = append(b, ui.pages[ui.activePage].FullHelp()...)
101	}
102	b = append(b, []key.Binding{
103		ui.common.KeyMap.Quit,
104		ui.common.KeyMap.Help,
105	})
106	return b
107}
108
109// SetSize implements common.Component.
110func (ui *UI) SetSize(width, height int) {
111	ui.common.SetSize(width, height)
112	wm, hm := ui.getMargins()
113	ui.header.SetSize(width-wm, height-hm)
114	ui.footer.SetSize(width-wm, height-hm)
115	for _, p := range ui.pages {
116		if p != nil {
117			p.SetSize(width-wm, height-hm)
118		}
119	}
120}
121
122// Init implements tea.Model.
123func (ui *UI) Init() tea.Cmd {
124	ui.pages[selectionPage] = selection.New(ui.s, ui.common)
125	ui.pages[repoPage] = repo.New(ui.s, ui.common)
126	ui.SetSize(ui.common.Width, ui.common.Height)
127	cmds := make([]tea.Cmd, 0)
128	cmds = append(cmds,
129		ui.pages[selectionPage].Init(),
130		ui.pages[repoPage].Init(),
131	)
132	if ui.initialRepo != "" {
133		cmds = append(cmds, ui.initialRepoCmd(ui.initialRepo))
134	}
135	ui.state = loadedState
136	ui.SetSize(ui.common.Width, ui.common.Height)
137	return tea.Batch(cmds...)
138}
139
140// Update implements tea.Model.
141func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
142	log.Printf("msg: %T", msg)
143	cmds := make([]tea.Cmd, 0)
144	switch msg := msg.(type) {
145	case tea.WindowSizeMsg:
146		ui.SetSize(msg.Width, msg.Height)
147		for i, p := range ui.pages {
148			m, cmd := p.Update(msg)
149			ui.pages[i] = m.(common.Page)
150			if cmd != nil {
151				cmds = append(cmds, cmd)
152			}
153		}
154	case tea.KeyMsg, tea.MouseMsg:
155		switch msg := msg.(type) {
156		case tea.KeyMsg:
157			switch {
158			case key.Matches(msg, ui.common.KeyMap.Back) && ui.error != nil:
159				ui.error = nil
160				ui.state = loadedState
161			case key.Matches(msg, ui.common.KeyMap.Help):
162				ui.footer.SetShowAll(!ui.footer.ShowAll())
163			case key.Matches(msg, ui.common.KeyMap.Quit):
164				return ui, tea.Quit
165			case ui.activePage == repoPage && key.Matches(msg, ui.common.KeyMap.Back):
166				ui.activePage = selectionPage
167			}
168		}
169	case common.ErrorMsg:
170		ui.error = msg
171		ui.state = errorState
172		return ui, nil
173	case selector.SelectMsg:
174		switch msg.IdentifiableItem.(type) {
175		case selection.Item:
176			if ui.activePage == selectionPage {
177				cmds = append(cmds, ui.setRepoCmd(msg.ID()))
178			}
179		}
180	}
181	h, cmd := ui.header.Update(msg)
182	ui.header = h.(*header.Header)
183	if cmd != nil {
184		cmds = append(cmds, cmd)
185	}
186	f, cmd := ui.footer.Update(msg)
187	ui.footer = f.(*footer.Footer)
188	if cmd != nil {
189		cmds = append(cmds, cmd)
190	}
191	if ui.state == loadedState {
192		m, cmd := ui.pages[ui.activePage].Update(msg)
193		ui.pages[ui.activePage] = m.(common.Page)
194		if cmd != nil {
195			cmds = append(cmds, cmd)
196		}
197	}
198	// This fixes determining the height margin of the footer.
199	ui.SetSize(ui.common.Width, ui.common.Height)
200	return ui, tea.Batch(cmds...)
201}
202
203// View implements tea.Model.
204func (ui *UI) View() string {
205	var view string
206	wm, hm := ui.getMargins()
207	footer := ui.footer.View()
208	style := ui.common.Styles.App.Copy()
209	switch ui.state {
210	case startState:
211		view = "Loading..."
212	case errorState:
213		err := ui.common.Styles.ErrorTitle.Render("Bummer")
214		err += ui.common.Styles.ErrorBody.Render(ui.error.Error())
215		view = ui.common.Styles.Error.Copy().
216			Width(ui.common.Width -
217				wm -
218				ui.common.Styles.ErrorBody.GetHorizontalFrameSize()).
219			Height(ui.common.Height -
220				hm -
221				ui.common.Styles.Error.GetVerticalFrameSize()).
222			Render(err)
223	case loadedState:
224		view = ui.pages[ui.activePage].View()
225	default:
226		view = "Unknown state :/ this is a bug!"
227	}
228	switch ui.activePage {
229	case selectionPage:
230		view = lipgloss.JoinVertical(lipgloss.Bottom,
231			ui.header.View(),
232			view,
233		)
234	case repoPage:
235	}
236	return style.Render(
237		lipgloss.JoinVertical(lipgloss.Bottom,
238			view,
239			footer,
240		),
241	)
242}
243
244func (ui *UI) setRepoCmd(rn string) tea.Cmd {
245	rs := ui.s.Source()
246	return func() tea.Msg {
247		for _, r := range rs.AllRepos() {
248			if r.Repo() == rn {
249				ui.activePage = repoPage
250				return repo.RepoMsg(r)
251			}
252		}
253		return common.ErrorMsg(git.ErrMissingRepo)
254	}
255}
256
257func (ui *UI) initialRepoCmd(rn string) tea.Cmd {
258	rs := ui.s.Source()
259	return func() tea.Msg {
260		for _, r := range rs.AllRepos() {
261			if r.Repo() == rn {
262				ui.activePage = repoPage
263				return repo.RepoMsg(r)
264			}
265		}
266		return nil
267	}
268}