ui.go

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