splash.go

  1package splash
  2
  3import (
  4	"github.com/charmbracelet/bubbles/v2/key"
  5	tea "github.com/charmbracelet/bubbletea/v2"
  6	"github.com/charmbracelet/crush/internal/config"
  7	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
  8	"github.com/charmbracelet/crush/internal/tui/components/core/list"
  9	"github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
 10	"github.com/charmbracelet/crush/internal/tui/components/logo"
 11	"github.com/charmbracelet/crush/internal/tui/styles"
 12	"github.com/charmbracelet/crush/internal/tui/util"
 13	"github.com/charmbracelet/crush/internal/version"
 14	"github.com/charmbracelet/lipgloss/v2"
 15)
 16
 17type Splash interface {
 18	util.Model
 19	layout.Sizeable
 20	layout.Help
 21}
 22
 23const (
 24	SplashScreenPaddingX = 2 // Padding X for the splash screen
 25	SplashScreenPaddingY = 1 // Padding Y for the splash screen
 26)
 27
 28type SplashScreenState string
 29
 30const (
 31	SplashScreenStateOnboarding SplashScreenState = "onboarding"
 32	SplashScreenStateInitialize SplashScreenState = "initialize"
 33	SplashScreenStateReady      SplashScreenState = "ready"
 34)
 35
 36// OnboardingCompleteMsg is sent when onboarding is complete
 37type OnboardingCompleteMsg struct{}
 38
 39type splashCmp struct {
 40	width, height        int
 41	keyMap               KeyMap
 42	logoRendered         string
 43	state                SplashScreenState
 44	modelList            *models.ModelListComponent
 45	cursorRow, cursorCol int
 46}
 47
 48func New() Splash {
 49	keyMap := DefaultKeyMap()
 50	listKeyMap := list.DefaultKeyMap()
 51	listKeyMap.Down.SetEnabled(false)
 52	listKeyMap.Up.SetEnabled(false)
 53	listKeyMap.HalfPageDown.SetEnabled(false)
 54	listKeyMap.HalfPageUp.SetEnabled(false)
 55	listKeyMap.Home.SetEnabled(false)
 56	listKeyMap.End.SetEnabled(false)
 57	listKeyMap.DownOneItem = keyMap.Next
 58	listKeyMap.UpOneItem = keyMap.Previous
 59
 60	t := styles.CurrentTheme()
 61	inputStyle := t.S().Base.Padding(0, 1, 0, 1)
 62	modelList := models.NewModelListComponent(listKeyMap, inputStyle, "Find your fave")
 63	return &splashCmp{
 64		width:        0,
 65		height:       0,
 66		keyMap:       keyMap,
 67		state:        SplashScreenStateOnboarding,
 68		logoRendered: "",
 69		modelList:    modelList,
 70	}
 71}
 72
 73// GetSize implements SplashPage.
 74func (s *splashCmp) GetSize() (int, int) {
 75	return s.width, s.height
 76}
 77
 78// Init implements SplashPage.
 79func (s *splashCmp) Init() tea.Cmd {
 80	if config.HasInitialDataConfig() {
 81		if b, _ := config.ProjectNeedsInitialization(); b {
 82			s.state = SplashScreenStateInitialize
 83		} else {
 84			s.state = SplashScreenStateReady
 85		}
 86	}
 87	return s.modelList.Init()
 88}
 89
 90// SetSize implements SplashPage.
 91func (s *splashCmp) SetSize(width int, height int) tea.Cmd {
 92	s.width = width
 93	s.height = height
 94	s.logoRendered = s.logoBlock()
 95	listHeigh := min(40, height-(SplashScreenPaddingY*2)-lipgloss.Height(s.logoRendered)-2) // -1 for the title
 96	listWidth := min(60, width-(SplashScreenPaddingX*2))
 97
 98	// Calculate the cursor position based on the height and logo size
 99	s.cursorRow = height - listHeigh
100	return s.modelList.SetSize(listWidth, listHeigh)
101}
102
103// Update implements SplashPage.
104func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
105	switch msg := msg.(type) {
106	case tea.WindowSizeMsg:
107		return s, s.SetSize(msg.Width, msg.Height)
108	case tea.KeyPressMsg:
109		switch {
110		default:
111			u, cmd := s.modelList.Update(msg)
112			s.modelList = u
113			return s, cmd
114		}
115	}
116	return s, nil
117}
118
119// View implements SplashPage.
120func (s *splashCmp) View() tea.View {
121	t := styles.CurrentTheme()
122	var cursor *tea.Cursor
123
124	var content string
125	switch s.state {
126	case SplashScreenStateOnboarding:
127		// Show logo and model selector
128		remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
129		modelListView := s.modelList.View()
130		cursor = s.moveCursor(modelListView.Cursor())
131		modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
132			lipgloss.JoinVertical(
133				lipgloss.Left,
134				t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("Choose a Model"),
135				"",
136				modelListView.String(),
137			),
138		)
139		content = lipgloss.JoinVertical(
140			lipgloss.Left,
141			s.logoRendered,
142			modelSelector,
143		)
144	default:
145		// Show just the logo for other states
146		content = s.logoRendered
147	}
148
149	view := tea.NewView(
150		t.S().Base.
151			Width(s.width).
152			Height(s.height).
153			PaddingTop(SplashScreenPaddingY).
154			PaddingLeft(SplashScreenPaddingX).
155			PaddingRight(SplashScreenPaddingX).
156			PaddingBottom(SplashScreenPaddingY).
157			Render(content),
158	)
159
160	view.SetCursor(cursor)
161	return view
162}
163
164func (s *splashCmp) logoBlock() string {
165	t := styles.CurrentTheme()
166	const padding = 2
167	return logo.Render(version.Version, false, logo.Opts{
168		FieldColor:   t.Primary,
169		TitleColorA:  t.Secondary,
170		TitleColorB:  t.Primary,
171		CharmColor:   t.Secondary,
172		VersionColor: t.Primary,
173		Width:        s.width - (SplashScreenPaddingX * 2),
174	})
175}
176
177func (m *splashCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
178	if cursor == nil {
179		return nil
180	}
181	offset := m.cursorRow
182	cursor.Y += offset
183	cursor.X = cursor.X + 3 // 3 for padding
184	return cursor
185}
186
187// Bindings implements SplashPage.
188func (s *splashCmp) Bindings() []key.Binding {
189	if s.state == SplashScreenStateOnboarding {
190		return []key.Binding{
191			s.keyMap.Select,
192			s.keyMap.Next,
193			s.keyMap.Previous,
194		}
195	}
196	return []key.Binding{}
197}