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	ui.SetSize(c.Width, c.Height)
 61	return ui
 62}
 63
 64func (ui *UI) getMargins() (wm, hm int) {
 65	wm = ui.common.Styles.App.GetHorizontalFrameSize()
 66	hm = ui.common.Styles.App.GetVerticalFrameSize() +
 67		ui.common.Styles.Header.GetHeight() +
 68		ui.footer.Height()
 69	return
 70}
 71
 72// ShortHelp implements help.KeyMap.
 73func (ui *UI) ShortHelp() []key.Binding {
 74	b := make([]key.Binding, 0)
 75	switch ui.state {
 76	case errorState:
 77		b = append(b, ui.common.KeyMap.Back)
 78	case loadedState:
 79		b = append(b, ui.pages[ui.activePage].ShortHelp()...)
 80	}
 81	b = append(b,
 82		ui.common.KeyMap.Quit,
 83		ui.common.KeyMap.Help,
 84	)
 85	return b
 86}
 87
 88// FullHelp implements help.KeyMap.
 89func (ui *UI) FullHelp() [][]key.Binding {
 90	b := make([][]key.Binding, 0)
 91	switch ui.state {
 92	case errorState:
 93		b = append(b, []key.Binding{ui.common.KeyMap.Back})
 94	case loadedState:
 95		b = append(b, ui.pages[ui.activePage].FullHelp()...)
 96	}
 97	b = append(b, []key.Binding{
 98		ui.common.KeyMap.Quit,
 99		ui.common.KeyMap.Help,
100	})
101	return b
102}
103
104// SetSize implements common.Component.
105func (ui *UI) SetSize(width, height int) {
106	ui.common.SetSize(width, height)
107	wm, hm := ui.getMargins()
108	ui.header.SetSize(width-wm, height-hm)
109	ui.footer.SetSize(width-wm, height-hm)
110	for _, p := range ui.pages {
111		if p != nil {
112			p.SetSize(width-wm, height-hm)
113		}
114	}
115}
116
117// Init implements tea.Model.
118func (ui *UI) Init() tea.Cmd {
119	ui.pages[selectionPage] = selection.New(ui.s, ui.common)
120	ui.pages[repoPage] = repo.New(ui.s, ui.common)
121	ui.SetSize(ui.common.Width, ui.common.Height)
122	cmds := make([]tea.Cmd, 0)
123	cmds = append(cmds,
124		ui.pages[selectionPage].Init(),
125		ui.pages[repoPage].Init(),
126	)
127	if ui.initialRepo != "" {
128		cmds = append(cmds, ui.initialRepoCmd(ui.initialRepo))
129	}
130	ui.state = loadedState
131	return tea.Batch(cmds...)
132}
133
134// Update implements tea.Model.
135func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
136	log.Printf("msg: %T", msg)
137	cmds := make([]tea.Cmd, 0)
138	switch msg := msg.(type) {
139	case tea.WindowSizeMsg:
140		ui.SetSize(msg.Width, msg.Height)
141		for i, p := range ui.pages {
142			m, cmd := p.Update(msg)
143			ui.pages[i] = m.(common.Page)
144			if cmd != nil {
145				cmds = append(cmds, cmd)
146			}
147		}
148	case tea.KeyMsg, tea.MouseMsg:
149		switch msg := msg.(type) {
150		case tea.KeyMsg:
151			switch {
152			case key.Matches(msg, ui.common.KeyMap.Back) && ui.error != nil:
153				ui.error = nil
154				ui.state = loadedState
155			case key.Matches(msg, ui.common.KeyMap.Help):
156				ui.footer.SetShowAll(!ui.footer.ShowAll())
157			case key.Matches(msg, ui.common.KeyMap.Quit):
158				return ui, tea.Quit
159			case ui.activePage == repoPage && key.Matches(msg, ui.common.KeyMap.Back):
160				ui.activePage = selectionPage
161			}
162		}
163	case common.ErrorMsg:
164		ui.error = msg
165		ui.state = errorState
166		return ui, nil
167	case selector.SelectMsg:
168		switch msg.IdentifiableItem.(type) {
169		case selection.Item:
170			if ui.activePage == selectionPage {
171				cmds = append(cmds, ui.setRepoCmd(msg.ID()))
172			}
173		}
174	}
175	h, cmd := ui.header.Update(msg)
176	ui.header = h.(*header.Header)
177	if cmd != nil {
178		cmds = append(cmds, cmd)
179	}
180	f, cmd := ui.footer.Update(msg)
181	ui.footer = f.(*footer.Footer)
182	if cmd != nil {
183		cmds = append(cmds, cmd)
184	}
185	if ui.state == loadedState {
186		m, cmd := ui.pages[ui.activePage].Update(msg)
187		ui.pages[ui.activePage] = m.(common.Page)
188		if cmd != nil {
189			cmds = append(cmds, cmd)
190		}
191	}
192	// This fixes determining the height margin of the footer.
193	ui.SetSize(ui.common.Width, ui.common.Height)
194	return ui, tea.Batch(cmds...)
195}
196
197// View implements tea.Model.
198func (ui *UI) View() string {
199	var view string
200	wm, hm := ui.getMargins()
201	footer := ui.footer.View()
202	style := ui.common.Styles.App.Copy()
203	switch ui.state {
204	case startState:
205		view = "Loading..."
206	case errorState:
207		err := ui.common.Styles.ErrorTitle.Render("Bummer")
208		err += ui.common.Styles.ErrorBody.Render(ui.error.Error())
209		view = ui.common.Styles.Error.Copy().
210			Width(ui.common.Width -
211				wm -
212				ui.common.Styles.ErrorBody.GetHorizontalFrameSize()).
213			Height(ui.common.Height -
214				hm -
215				ui.common.Styles.Error.GetVerticalFrameSize()).
216			Render(err)
217	case loadedState:
218		view = ui.pages[ui.activePage].View()
219	default:
220		view = "Unknown state :/ this is a bug!"
221	}
222	return style.Render(
223		lipgloss.JoinVertical(lipgloss.Bottom,
224			ui.header.View(),
225			view,
226			footer,
227		),
228	)
229}
230
231func (ui *UI) setRepoCmd(rn string) tea.Cmd {
232	rs := ui.s.Source()
233	return func() tea.Msg {
234		for _, r := range rs.AllRepos() {
235			if r.Repo() == rn {
236				ui.activePage = repoPage
237				return repo.RepoMsg(r)
238			}
239		}
240		return common.ErrorMsg(git.ErrMissingRepo)
241	}
242}
243
244func (ui *UI) initialRepoCmd(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 nil
254	}
255}