1package models
  2
  3import (
  4	"fmt"
  5
  6	"github.com/charmbracelet/bubbles/v2/spinner"
  7	"github.com/charmbracelet/bubbles/v2/textinput"
  8	tea "github.com/charmbracelet/bubbletea/v2"
  9	"github.com/charmbracelet/crush/internal/config"
 10	"github.com/charmbracelet/crush/internal/home"
 11	"github.com/charmbracelet/crush/internal/tui/styles"
 12	"github.com/charmbracelet/lipgloss/v2"
 13)
 14
 15type APIKeyInputState int
 16
 17const (
 18	APIKeyInputStateInitial APIKeyInputState = iota
 19	APIKeyInputStateVerifying
 20	APIKeyInputStateVerified
 21	APIKeyInputStateError
 22)
 23
 24type APIKeyStateChangeMsg struct {
 25	State APIKeyInputState
 26}
 27
 28type APIKeyInput struct {
 29	input        textinput.Model
 30	width        int
 31	spinner      spinner.Model
 32	providerName string
 33	state        APIKeyInputState
 34	title        string
 35	showTitle    bool
 36}
 37
 38func NewAPIKeyInput() *APIKeyInput {
 39	t := styles.CurrentTheme()
 40
 41	ti := textinput.New()
 42	ti.Placeholder = "Enter your API key..."
 43	ti.SetVirtualCursor(false)
 44	ti.Prompt = "> "
 45	ti.SetStyles(t.S().TextInput)
 46	ti.Focus()
 47
 48	return &APIKeyInput{
 49		input: ti,
 50		state: APIKeyInputStateInitial,
 51		spinner: spinner.New(
 52			spinner.WithSpinner(spinner.Dot),
 53			spinner.WithStyle(t.S().Base.Foreground(t.Green)),
 54		),
 55		providerName: "Provider",
 56		showTitle:    true,
 57	}
 58}
 59
 60func (a *APIKeyInput) SetProviderName(name string) {
 61	a.providerName = name
 62	a.updateStatePresentation()
 63}
 64
 65func (a *APIKeyInput) SetShowTitle(show bool) {
 66	a.showTitle = show
 67}
 68
 69func (a *APIKeyInput) GetTitle() string {
 70	return a.title
 71}
 72
 73func (a *APIKeyInput) Init() tea.Cmd {
 74	a.updateStatePresentation()
 75	return a.spinner.Tick
 76}
 77
 78func (a *APIKeyInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 79	switch msg := msg.(type) {
 80	case spinner.TickMsg:
 81		if a.state == APIKeyInputStateVerifying {
 82			var cmd tea.Cmd
 83			a.spinner, cmd = a.spinner.Update(msg)
 84			a.updateStatePresentation()
 85			return a, cmd
 86		}
 87		return a, nil
 88	case APIKeyStateChangeMsg:
 89		a.state = msg.State
 90		var cmd tea.Cmd
 91		if msg.State == APIKeyInputStateVerifying {
 92			cmd = a.spinner.Tick
 93		}
 94		a.updateStatePresentation()
 95		return a, cmd
 96	}
 97
 98	var cmd tea.Cmd
 99	a.input, cmd = a.input.Update(msg)
100	return a, cmd
101}
102
103func (a *APIKeyInput) updateStatePresentation() {
104	t := styles.CurrentTheme()
105
106	prefixStyle := t.S().Base.
107		Foreground(t.Primary)
108	accentStyle := t.S().Base.Foreground(t.Green).Bold(true)
109	errorStyle := t.S().Base.Foreground(t.Cherry)
110
111	switch a.state {
112	case APIKeyInputStateInitial:
113		titlePrefix := prefixStyle.Render("Enter your ")
114		a.title = titlePrefix + accentStyle.Render(a.providerName+" API Key") + prefixStyle.Render(".")
115		a.input.SetStyles(t.S().TextInput)
116		a.input.Prompt = "> "
117	case APIKeyInputStateVerifying:
118		titlePrefix := prefixStyle.Render("Verifying your ")
119		a.title = titlePrefix + accentStyle.Render(a.providerName+" API Key") + prefixStyle.Render("...")
120		ts := t.S().TextInput
121		// make the blurred state be the same
122		ts.Blurred.Prompt = ts.Focused.Prompt
123		a.input.Prompt = a.spinner.View()
124		a.input.Blur()
125	case APIKeyInputStateVerified:
126		a.title = accentStyle.Render(a.providerName+" API Key") + prefixStyle.Render(" validated.")
127		ts := t.S().TextInput
128		// make the blurred state be the same
129		ts.Blurred.Prompt = ts.Focused.Prompt
130		a.input.SetStyles(ts)
131		a.input.Prompt = styles.CheckIcon + " "
132		a.input.Blur()
133	case APIKeyInputStateError:
134		a.title = errorStyle.Render("Invalid ") + accentStyle.Render(a.providerName+" API Key") + errorStyle.Render(". Try again?")
135		ts := t.S().TextInput
136		ts.Focused.Prompt = ts.Focused.Prompt.Foreground(t.Cherry)
137		a.input.Focus()
138		a.input.SetStyles(ts)
139		a.input.Prompt = styles.ErrorIcon + " "
140	}
141}
142
143func (a *APIKeyInput) View() string {
144	inputView := a.input.View()
145
146	dataPath := config.GlobalConfigData()
147	dataPath = home.Short(dataPath)
148	helpText := styles.CurrentTheme().S().Muted.
149		Render(fmt.Sprintf("This will be written to the global configuration: %s", dataPath))
150
151	var content string
152	if a.showTitle && a.title != "" {
153		content = lipgloss.JoinVertical(
154			lipgloss.Left,
155			a.title,
156			"",
157			inputView,
158			"",
159			helpText,
160		)
161	} else {
162		content = lipgloss.JoinVertical(
163			lipgloss.Left,
164			inputView,
165			"",
166			helpText,
167		)
168	}
169
170	return content
171}
172
173func (a *APIKeyInput) Cursor() *tea.Cursor {
174	cursor := a.input.Cursor()
175	if cursor != nil && a.showTitle {
176		cursor.Y += 2 // Adjust for title and spacing
177	}
178	return cursor
179}
180
181func (a *APIKeyInput) Value() string {
182	return a.input.Value()
183}
184
185func (a *APIKeyInput) Tick() tea.Cmd {
186	if a.state == APIKeyInputStateVerifying {
187		return a.spinner.Tick
188	}
189	return nil
190}
191
192func (a *APIKeyInput) SetWidth(width int) {
193	a.width = width
194	a.input.SetWidth(width - 4)
195}
196
197func (a *APIKeyInput) Reset() {
198	a.state = APIKeyInputStateInitial
199	a.input.SetValue("")
200	a.input.Focus()
201	a.updateStatePresentation()
202}