apikey.go

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