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/crush/internal/tui/util"
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) (util.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 = home.Short(dataPath)
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}