ui.go

  1package ui
  2
  3import (
  4	"strings"
  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/pages/repo"
 14	"github.com/charmbracelet/soft-serve/ui/pages/selection"
 15	"github.com/charmbracelet/soft-serve/ui/session"
 16)
 17
 18type sessionState int
 19
 20const (
 21	startState sessionState = iota
 22	errorState
 23	loadedState
 24)
 25
 26// UI is the main UI model.
 27type UI struct {
 28	s          session.Session
 29	common     common.Common
 30	pages      []common.Page
 31	activePage int
 32	state      sessionState
 33	header     *header.Header
 34	footer     *footer.Footer
 35	error      error
 36}
 37
 38// New returns a new UI model.
 39func New(s session.Session, c common.Common, initialRepo string) *UI {
 40	h := header.New(c, s.Config().Name)
 41	ui := &UI{
 42		s:          s,
 43		common:     c,
 44		pages:      make([]common.Page, 2), // selection & repo
 45		activePage: 0,
 46		state:      startState,
 47		header:     h,
 48	}
 49	ui.footer = footer.New(c, ui)
 50	ui.SetSize(c.Width, c.Height)
 51	return ui
 52}
 53
 54func (ui *UI) getMargins() (wm, hm int) {
 55	wm = ui.common.Styles.App.GetHorizontalFrameSize()
 56	hm = ui.common.Styles.App.GetVerticalFrameSize() +
 57		ui.common.Styles.Header.GetHeight() +
 58		ui.common.Styles.Footer.GetHeight()
 59	return
 60}
 61
 62// ShortHelp implements help.KeyMap.
 63func (ui *UI) ShortHelp() []key.Binding {
 64	b := make([]key.Binding, 0)
 65	b = append(b, ui.pages[ui.activePage].ShortHelp()...)
 66	b = append(b, ui.common.Keymap.Quit)
 67	return b
 68}
 69
 70// FullHelp implements help.KeyMap.
 71func (ui *UI) FullHelp() [][]key.Binding {
 72	b := make([][]key.Binding, 0)
 73	b = append(b, ui.pages[ui.activePage].FullHelp()...)
 74	b = append(b, []key.Binding{ui.common.Keymap.Quit})
 75	return b
 76}
 77
 78// SetSize implements common.Component.
 79func (ui *UI) SetSize(width, height int) {
 80	ui.common.SetSize(width, height)
 81	wm, hm := ui.getMargins()
 82	ui.header.SetSize(width-wm, height-hm)
 83	ui.footer.SetSize(width-wm, height-hm)
 84	for _, p := range ui.pages {
 85		if p != nil {
 86			p.SetSize(width-wm, height-hm)
 87		}
 88	}
 89}
 90
 91// Init implements tea.Model.
 92func (ui *UI) Init() tea.Cmd {
 93	cfg := ui.s.Config()
 94	ui.pages[0] = selection.New(ui.s, ui.common)
 95	ui.pages[1] = repo.New(ui.common, &source{cfg.Source})
 96	ui.SetSize(ui.common.Width, ui.common.Height)
 97	ui.state = loadedState
 98	return tea.Batch(
 99		ui.pages[0].Init(),
100		ui.pages[1].Init(),
101	)
102}
103
104// Update implements tea.Model.
105// TODO update help when page change.
106func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
107	cmds := make([]tea.Cmd, 0)
108	switch msg := msg.(type) {
109	case tea.WindowSizeMsg:
110		h, cmd := ui.header.Update(msg)
111		ui.header = h.(*header.Header)
112		if cmd != nil {
113			cmds = append(cmds, cmd)
114		}
115		f, cmd := ui.footer.Update(msg)
116		ui.footer = f.(*footer.Footer)
117		if cmd != nil {
118			cmds = append(cmds, cmd)
119		}
120		for i, p := range ui.pages {
121			m, cmd := p.Update(msg)
122			ui.pages[i] = m.(common.Page)
123			if cmd != nil {
124				cmds = append(cmds, cmd)
125			}
126		}
127		ui.SetSize(msg.Width, msg.Height)
128	case tea.KeyMsg:
129		switch {
130		case key.Matches(msg, ui.common.Keymap.Quit):
131			return ui, tea.Quit
132		case ui.activePage == 1 && key.Matches(msg, ui.common.Keymap.Back):
133			ui.activePage = 0
134		}
135	case common.ErrorMsg:
136		ui.error = msg
137		ui.state = errorState
138		return ui, nil
139	case selector.SelectMsg:
140		ui.activePage = (ui.activePage + 1) % 2
141	}
142	m, cmd := ui.pages[ui.activePage].Update(msg)
143	ui.pages[ui.activePage] = m.(common.Page)
144	if cmd != nil {
145		cmds = append(cmds, cmd)
146	}
147	return ui, tea.Batch(cmds...)
148}
149
150// View implements tea.Model.
151func (ui *UI) View() string {
152	s := strings.Builder{}
153	switch ui.state {
154	case startState:
155		s.WriteString("Loading...")
156	case errorState:
157		err := ui.common.Styles.ErrorTitle.Render("Bummer")
158		err += ui.common.Styles.ErrorBody.Render(ui.error.Error())
159		s.WriteString(err)
160	case loadedState:
161		s.WriteString(lipgloss.JoinVertical(
162			lipgloss.Bottom,
163			ui.header.View(),
164			ui.pages[ui.activePage].View(),
165			ui.footer.View(),
166		))
167	default:
168		s.WriteString("Unknown state :/ this is a bug!")
169	}
170	return ui.common.Styles.App.Render(s.String())
171}