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}