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)
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}