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