v0.0.51 c610a3a

login_dialog.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close