@@ -0,0 +1,302 @@
8+ "charm.land/bubbles/v2/help"
9+ "charm.land/bubbles/v2/key"
10+ "charm.land/bubbles/v2/spinner"
11+ "charm.land/bubbles/v2/textinput"
12+ tea "charm.land/bubbletea/v2"
13+ "github.com/charmbracelet/catwalk/pkg/catwalk"
14+ "github.com/charmbracelet/crush/internal/config"
15+ "github.com/charmbracelet/crush/internal/ui/common"
16+ "github.com/charmbracelet/crush/internal/ui/styles"
17+ "github.com/charmbracelet/crush/internal/uiutil"
18+ uv "github.com/charmbracelet/ultraviolet"
19+ "github.com/charmbracelet/x/exp/charmtone"
22+type APIKeyInputState int
25+ APIKeyInputStateInitial APIKeyInputState = iota
26+ APIKeyInputStateVerifying
27+ APIKeyInputStateVerified
28+ APIKeyInputStateError
31+// APIKeyInputID is the identifier for the model selection dialog.
32+const APIKeyInputID = "api_key_input"
34+// APIKeyInput represents a model selection dialog.
35+type APIKeyInput struct {
38+ provider catwalk.Provider
39+ model config.SelectedModel
40+ modelType config.SelectedModelType
43+ state APIKeyInputState
49+ input textinput.Model
50+ spinner spinner.Model
54+var _ Dialog = (*APIKeyInput)(nil)
56+// NewAPIKeyInput creates a new Models dialog.
57+func NewAPIKeyInput(com *common.Common, provider catwalk.Provider, model config.SelectedModel, modelType config.SelectedModelType) (*APIKeyInput, error) {
62+ m.provider = provider
64+ m.modelType = modelType
67+ innerWidth := m.width - t.Dialog.View.GetHorizontalFrameSize() - 2
69+ m.input = textinput.New()
70+ m.input.SetVirtualCursor(false)
71+ m.input.Placeholder = "Enter you API key..."
72+ m.input.SetStyles(com.Styles.TextInput)
74+ m.input.SetWidth(innerWidth - t.Dialog.InputPrompt.GetHorizontalFrameSize() - 1) // (1) cursor padding
76+ m.spinner = spinner.New(
77+ spinner.WithSpinner(spinner.Dot),
78+ spinner.WithStyle(t.Base.Foreground(t.Green)),
82+ m.help.Styles = t.DialogHelpStyles()
84+ m.keyMap.Submit = key.NewBinding(
85+ key.WithKeys("enter", "ctrl+y"),
86+ key.WithHelp("enter", "submit"),
88+ m.keyMap.Close = CloseKey
93+// ID implements Dialog.
94+func (m *APIKeyInput) ID() string {
98+// Update implements tea.Model.
99+func (m *APIKeyInput) HandleMsg(msg tea.Msg) Action {
100+ switch msg := msg.(type) {
101+ case ActionChangeAPIKeyState:
104+ case APIKeyInputStateVerifying:
105+ cmd := tea.Batch(m.spinner.Tick, m.verifyAPIKey)
106+ return ActionCmd{cmd}
108+ case spinner.TickMsg:
110+ case APIKeyInputStateVerifying:
112+ m.spinner, cmd = m.spinner.Update(msg)
114+ return ActionCmd{cmd}
117+ case tea.KeyPressMsg:
119+ case m.state == APIKeyInputStateVerifying:
121+ case key.Matches(msg, m.keyMap.Close):
123+ case APIKeyInputStateVerified:
124+ return m.saveKeyAndContinue()
126+ return ActionClose{}
128+ case key.Matches(msg, m.keyMap.Submit):
130+ case APIKeyInputStateInitial, APIKeyInputStateError:
131+ return ActionChangeAPIKeyState{State: APIKeyInputStateVerifying}
132+ case APIKeyInputStateVerified:
133+ return m.saveKeyAndContinue()
137+ m.input, cmd = m.input.Update(msg)
139+ return ActionCmd{cmd}
144+ m.input, cmd = m.input.Update(msg)
146+ return ActionCmd{cmd}
152+// View implements tea.Model.
153+func (m *APIKeyInput) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
156+ textStyle := t.Dialog.SecondaryText
157+ helpStyle := t.Dialog.HelpView
158+ dialogStyle := t.Dialog.View.Width(m.width)
159+ inputStyle := t.Dialog.InputPrompt
160+ helpStyle = helpStyle.Width(m.width - dialogStyle.GetHorizontalFrameSize())
162+ m.input.Prompt = m.spinner.View()
164+ content := strings.Join([]string{
166+ inputStyle.Render(m.inputView()),
167+ textStyle.Render("This will be written in your global configuration:"),
168+ textStyle.Render(config.GlobalConfigData()),
170+ helpStyle.Render(m.help.View(m)),
173+ view := dialogStyle.Render(content)
176+ DrawCenterCursor(scr, area, view, cur)
180+func (m *APIKeyInput) headerView() string {
182+ titleStyle := t.Dialog.Title
183+ dialogStyle := t.Dialog.View.Width(m.width)
185+ headerOffset := titleStyle.GetHorizontalFrameSize() + dialogStyle.GetHorizontalFrameSize()
186+ return common.DialogTitle(t, titleStyle.Render(m.dialogTitle()), m.width-headerOffset)
189+func (m *APIKeyInput) dialogTitle() string {
191+ textStyle := t.Dialog.TitleText
192+ errorStyle := t.Dialog.TitleError
193+ accentStyle := t.Dialog.TitleAccent
196+ case APIKeyInputStateInitial:
197+ return textStyle.Render("Enter your ") + accentStyle.Render(fmt.Sprintf("%s Key", m.provider.Name)) + textStyle.Render(".")
198+ case APIKeyInputStateVerifying:
199+ return textStyle.Render("Verifying your ") + accentStyle.Render(fmt.Sprintf("%s Key", m.provider.Name)) + textStyle.Render("...")
200+ case APIKeyInputStateVerified:
201+ return accentStyle.Render(fmt.Sprintf("%s Key", m.provider.Name)) + textStyle.Render(" validated.")
202+ case APIKeyInputStateError:
203+ return errorStyle.Render("Invalid ") + accentStyle.Render(fmt.Sprintf("%s Key", m.provider.Name)) + errorStyle.Render(". Try again?")
208+func (m *APIKeyInput) inputView() string {
212+ case APIKeyInputStateInitial:
213+ m.input.Prompt = "> "
214+ m.input.SetStyles(t.TextInput)
216+ case APIKeyInputStateVerifying:
218+ ts.Blurred.Prompt = ts.Focused.Prompt
220+ m.input.Prompt = m.spinner.View()
221+ m.input.SetStyles(ts)
223+ case APIKeyInputStateVerified:
225+ ts.Blurred.Prompt = ts.Focused.Prompt
227+ m.input.Prompt = styles.CheckIcon + " "
228+ m.input.SetStyles(ts)
230+ case APIKeyInputStateError:
232+ ts.Focused.Prompt = ts.Focused.Prompt.Foreground(charmtone.Cherry)
234+ m.input.Prompt = styles.ErrorIcon + " "
235+ m.input.SetStyles(ts)
238+ return m.input.View()
241+// Cursor returns the cursor position relative to the dialog.
242+func (c *APIKeyInput) Cursor() *tea.Cursor {
243+ return InputCursor(c.com.Styles, c.input.Cursor())
246+// FullHelp returns the full help view.
247+func (m *APIKeyInput) FullHelp() [][]key.Binding {
248+ return [][]key.Binding{
256+// ShortHelp returns the full help view.
257+func (m *APIKeyInput) ShortHelp() []key.Binding {
258+ return []key.Binding{
264+func (m *APIKeyInput) verifyAPIKey() tea.Msg {
267+ providerConfig := config.ProviderConfig{
268+ ID: string(m.provider.ID),
269+ Name: m.provider.Name,
270+ APIKey: m.input.Value(),
271+ Type: m.provider.Type,
272+ BaseURL: m.provider.APIEndpoint,
274+ err := providerConfig.TestConnection(config.Get().Resolver())
276+ // intentionally wait for at least 750ms to make sure the user sees the spinner
277+ elapsed := time.Since(start)
278+ minimum := 750 * time.Millisecond
279+ if elapsed < minimum {
280+ time.Sleep(minimum - elapsed)
284+ return ActionChangeAPIKeyState{APIKeyInputStateVerified}
286+ return ActionChangeAPIKeyState{APIKeyInputStateError}
289+func (m *APIKeyInput) saveKeyAndContinue() tea.Msg {
290+ cfg := m.com.Config()
292+ err := cfg.SetProviderAPIKey(string(m.provider.ID), m.input.Value())
294+ return uiutil.ReportError(fmt.Errorf("failed to save API key: %w", err))
297+ return ActionSelectModel{
298+ Provider: m.provider,
300+ ModelType: m.modelType,