1package modes
2
3import (
4 "fmt"
5
6 "github.com/patriceckhart/zot/internal/tui"
7)
8
9// loginStep is the current node in the login dialog state machine.
10type loginStep int
11
12// loginStepClosed is the zero value on purpose: a freshly-constructed
13// dialog must default to closed so nothing shows up until Open() is
14// explicitly called.
15const (
16 loginStepClosed loginStep = iota
17 loginStepMethod // pick apikey vs subscription
18 loginStepProvider // pick anthropic vs openai
19 loginStepWaiting // browser open, waiting for callback
20 loginStepDone // success or error, waiting for key to dismiss
21)
22
23// loginDialog is a tiny inline dialog rendered above the editor while
24// the user picks their login method and provider.
25type loginDialog struct {
26 step loginStep
27 method string // "apikey" | "oauth"
28 provider string // "anthropic" | "openai"
29 message string
30 success bool
31 url string
32 cursor int
33}
34
35func newLoginDialog() *loginDialog {
36 return &loginDialog{}
37}
38
39// Active reports whether the dialog consumes input.
40func (d *loginDialog) Active() bool { return d != nil && d.step != loginStepClosed }
41
42// Open starts the dialog from scratch.
43func (d *loginDialog) Open() {
44 d.step = loginStepMethod
45 d.method = ""
46 d.provider = ""
47 d.message = ""
48 d.success = false
49 d.url = ""
50 d.cursor = 0
51}
52
53// Close hides the dialog.
54func (d *loginDialog) Close() {
55 d.step = loginStepClosed
56}
57
58// Render returns the dialog lines or nil when inactive.
59func (d *loginDialog) Render(th tui.Theme, width int) []string {
60 if !d.Active() {
61 return nil
62 }
63 var lines []string
64
65 switch d.step {
66 case loginStepMethod:
67 opts := []string{
68 "api key",
69 "subscription (claude pro/max · chatgpt plus/pro)",
70 }
71 lines = append(lines, frameHeader(th, "login", width))
72 lines = append(lines, th.FG256(th.Muted, "choose login method (↑/↓, enter, esc to cancel):"))
73 for i, o := range opts {
74 plain := " " + o
75 if i == d.cursor {
76 lines = append(lines, th.PadHighlight(plain, width))
77 } else {
78 lines = append(lines, th.FG256(th.Muted, plain))
79 }
80 }
81 lines = append(lines, frameRule(th, width))
82 case loginStepProvider:
83 opts := []string{"anthropic", "openai"}
84 lines = append(lines, frameHeader(th, "login · "+d.method, width))
85 lines = append(lines, th.FG256(th.Muted, "choose provider:"))
86 for i, o := range opts {
87 plain := " " + o
88 if i == d.cursor {
89 lines = append(lines, th.PadHighlight(plain, width))
90 } else {
91 lines = append(lines, th.FG256(th.Muted, plain))
92 }
93 }
94 lines = append(lines, frameRule(th, width))
95 case loginStepWaiting:
96 lines = append(lines, frameHeader(th, "login · "+d.method+" · "+d.provider, width))
97 lines = append(lines, th.FG256(th.FG, "opening browser..."))
98 lines = append(lines, th.FG256(th.Muted, d.url))
99 lines = append(lines, "")
100 lines = append(lines, th.FG256(th.Muted, "waiting for callback. press esc to cancel."))
101 lines = append(lines, frameRule(th, width))
102 case loginStepDone:
103 title := "login · failed"
104 body := th.FG256(th.Error, d.message)
105 if d.success {
106 title = "login · success"
107 body = th.FG256(th.Tool, fmt.Sprintf("logged in to %s via %s", d.provider, d.method))
108 }
109 lines = append(lines, frameHeader(th, title, width))
110 lines = append(lines, body)
111 lines = append(lines, th.FG256(th.Muted, "press any key to close"))
112 lines = append(lines, frameRule(th, width))
113 }
114 return lines
115}
116
117// Key is the result of handling a key press.
118type loginDialogAction struct {
119 StartAPIKey bool
120 StartOAuth bool
121 Provider string
122 Close bool
123}
124
125// HandleKey advances the dialog and returns an action to apply, if any.
126func (d *loginDialog) HandleKey(k tui.Key) loginDialogAction {
127 switch d.step {
128 case loginStepMethod:
129 return d.handleMethodKey(k)
130 case loginStepProvider:
131 return d.handleProviderKey(k)
132 case loginStepWaiting:
133 if k.Kind == tui.KeyEsc {
134 d.Close()
135 return loginDialogAction{Close: true}
136 }
137 case loginStepDone:
138 d.Close()
139 return loginDialogAction{Close: true}
140 }
141 return loginDialogAction{}
142}
143
144func (d *loginDialog) handleMethodKey(k tui.Key) loginDialogAction {
145 max := 2
146 switch k.Kind {
147 case tui.KeyUp:
148 if d.cursor > 0 {
149 d.cursor--
150 }
151 case tui.KeyDown:
152 if d.cursor < max-1 {
153 d.cursor++
154 }
155 case tui.KeyEsc:
156 d.Close()
157 return loginDialogAction{Close: true}
158 case tui.KeyEnter:
159 if d.cursor == 0 {
160 d.method = "apikey"
161 } else {
162 d.method = "oauth"
163 }
164 d.step = loginStepProvider
165 d.cursor = 0
166 }
167 return loginDialogAction{}
168}
169
170func (d *loginDialog) handleProviderKey(k tui.Key) loginDialogAction {
171 switch k.Kind {
172 case tui.KeyUp:
173 if d.cursor > 0 {
174 d.cursor--
175 }
176 case tui.KeyDown:
177 if d.cursor < 1 {
178 d.cursor++
179 }
180 case tui.KeyEsc:
181 d.Close()
182 return loginDialogAction{Close: true}
183 case tui.KeyEnter:
184 providers := []string{"anthropic", "openai"}
185 d.provider = providers[d.cursor]
186 d.step = loginStepWaiting
187 if d.method == "apikey" {
188 return loginDialogAction{StartAPIKey: true, Provider: d.provider}
189 }
190 return loginDialogAction{StartOAuth: true, Provider: d.provider}
191 }
192 return loginDialogAction{}
193}
194
195// ShowWaiting transitions to the waiting state with the given URL.
196// No-op if the user has already dismissed the dialog.
197func (d *loginDialog) ShowWaiting(url string) {
198 if d.step == loginStepClosed {
199 return
200 }
201 d.step = loginStepWaiting
202 d.url = url
203}
204
205// ShowResult transitions to the done state with the given outcome.
206// No-op if the user has already dismissed the dialog.
207func (d *loginDialog) ShowResult(success bool, message string) {
208 if d.step == loginStepClosed {
209 return
210 }
211 d.step = loginStepDone
212 d.success = success
213 d.message = message
214}
215
216func max0(x int) int {
217 if x < 0 {
218 return 0
219 }
220 return x
221}