Skip to content

Commit 3268557

Browse files
committed
Combine browser and auth-wait prompts in auth flow
1 parent 9c14931 commit 3268557

File tree

8 files changed

+287
-41
lines changed

8 files changed

+287
-41
lines changed

internal/auth/auth.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ func (a *Auth) GetToken(ctx context.Context) (string, error) {
4444
output.EmitLog(a.sink, "No existing credentials found. Please log in:")
4545
token, err := a.login.Login(ctx)
4646
if err != nil {
47+
if errors.Is(err, context.Canceled) {
48+
return "", err
49+
}
4750
output.EmitWarning(a.sink, "Authentication failed.")
4851
return "", err
4952
}

internal/auth/login.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,10 @@ func (l *loginProvider) Login(ctx context.Context) (string, error) {
3838
output.EmitLog(l.sink, fmt.Sprintf("Visit: %s", authURL))
3939
output.EmitLog(l.sink, fmt.Sprintf("Verification code: %s", authReq.Code))
4040

41-
// Ask whether to open the browser; ENTER or Y accepts (default yes), N skips
4241
browserCh := make(chan output.InputResponse, 1)
4342
output.EmitUserInputRequest(l.sink, output.UserInputRequestEvent{
44-
Prompt: "Open browser now?",
45-
Options: []output.InputOption{{Key: "y", Label: "Y"}, {Key: "n", Label: "n"}},
43+
Prompt: "Open browser?\nWaiting for authentication... (Press ENTER when complete)",
44+
Options: []output.InputOption{{Key: "y", Label: "Y"}, {Key: "n", Label: "n"}, {Key: "enter", Label: ""}},
4645
ResponseCh: browserCh,
4746
})
4847

@@ -51,10 +50,18 @@ func (l *loginProvider) Login(ctx context.Context) (string, error) {
5150
if resp.Cancelled {
5251
return "", context.Canceled
5352
}
54-
if resp.SelectedKey != "n" {
55-
if err := browser.OpenURL(authURL); err != nil {
56-
output.EmitLog(l.sink, fmt.Sprintf("Warning: Failed to open browser: %v", err))
57-
}
53+
if resp.SelectedKey == "n" {
54+
return "", context.Canceled
55+
}
56+
if resp.SelectedKey == "enter" {
57+
return l.completeAuth(ctx, authReq)
58+
}
59+
if resp.SelectedKey == "y" {
60+
go func() {
61+
if err := browser.OpenURL(authURL); err != nil {
62+
output.EmitLog(l.sink, fmt.Sprintf("Warning: Failed to open browser: %v", err))
63+
}
64+
}()
5865
}
5966
case <-ctx.Done():
6067
return "", ctx.Err()
@@ -63,7 +70,7 @@ func (l *loginProvider) Login(ctx context.Context) (string, error) {
6370
// Wait for the user to complete authentication in the browser
6471
enterCh := make(chan output.InputResponse, 1)
6572
output.EmitUserInputRequest(l.sink, output.UserInputRequestEvent{
66-
Prompt: "Waiting for authentication",
73+
Prompt: "Waiting for authentication...",
6774
Options: []output.InputOption{{Key: "enter", Label: "Press ENTER when complete"}},
6875
ResponseCh: enterCh,
6976
})

internal/output/format.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,30 @@ func formatProgressLine(e ProgressEvent) (string, bool) {
5858
}
5959

6060
func formatUserInputRequest(e UserInputRequestEvent) string {
61-
switch len(e.Options) {
61+
lines := strings.Split(e.Prompt, "\n")
62+
firstLine := lines[0]
63+
rest := lines[1:]
64+
labels := make([]string, 0, len(e.Options))
65+
for _, opt := range e.Options {
66+
if opt.Label != "" {
67+
labels = append(labels, opt.Label)
68+
}
69+
}
70+
71+
switch len(labels) {
6272
case 0:
63-
return e.Prompt
73+
if len(rest) == 0 {
74+
return firstLine
75+
}
76+
return strings.Join(append([]string{firstLine}, rest...), "\n")
6477
case 1:
65-
return fmt.Sprintf("%s (%s)", e.Prompt, e.Options[0].Label)
78+
firstLine = fmt.Sprintf("%s (%s)", firstLine, labels[0])
6679
default:
67-
labels := make([]string, len(e.Options))
68-
for i, opt := range e.Options {
69-
labels[i] = opt.Label
70-
}
71-
return fmt.Sprintf("%s [%s]", e.Prompt, strings.Join(labels, "/"))
80+
firstLine = fmt.Sprintf("%s [%s]", firstLine, strings.Join(labels, "/"))
81+
}
82+
83+
if len(rest) == 0 {
84+
return firstLine
7285
}
86+
return strings.Join(append([]string{firstLine}, rest...), "\n")
7387
}

internal/ui/app.go

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package ui
22

33
import (
44
"context"
5+
"fmt"
56
"strings"
67

78
tea "github.com/charmbracelet/bubbletea"
@@ -64,19 +65,21 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6465
}
6566
if a.pendingInput != nil {
6667
if msg.Type == tea.KeyEnter {
67-
// ENTER selects the first option (default)
68-
selectedKey := ""
69-
if len(a.pendingInput.Options) > 0 {
70-
selectedKey = a.pendingInput.Options[0].Key
68+
for _, opt := range a.pendingInput.Options {
69+
if opt.Key == "enter" {
70+
a.lines = appendLine(a.lines, formatResolvedInput(*a.pendingInput, "enter"))
71+
responseCmd := sendInputResponseCmd(a.pendingInput.ResponseCh, output.InputResponse{SelectedKey: "enter"})
72+
a.pendingInput = nil
73+
a.inputPrompt = a.inputPrompt.Hide()
74+
return a, responseCmd
75+
}
7176
}
72-
responseCmd := sendInputResponseCmd(a.pendingInput.ResponseCh, output.InputResponse{SelectedKey: selectedKey})
73-
a.pendingInput = nil
74-
a.inputPrompt = a.inputPrompt.Hide()
75-
return a, responseCmd
77+
return a, nil
7678
}
7779
// A single character key press selects the matching option
7880
for _, opt := range a.pendingInput.Options {
7981
if msg.String() == opt.Key {
82+
a.lines = appendLine(a.lines, formatResolvedInput(*a.pendingInput, opt.Key))
8083
responseCmd := sendInputResponseCmd(a.pendingInput.ResponseCh, output.InputResponse{SelectedKey: opt.Key})
8184
a.pendingInput = nil
8285
a.inputPrompt = a.inputPrompt.Hide()
@@ -122,6 +125,32 @@ func appendLine(lines []string, line string) []string {
122125
return lines
123126
}
124127

128+
func formatResolvedInput(req output.UserInputRequestEvent, selectedKey string) string {
129+
firstLine := strings.Split(req.Prompt, "\n")[0]
130+
labels := make([]string, 0, len(req.Options))
131+
selected := selectedKey
132+
133+
for _, opt := range req.Options {
134+
if opt.Label != "" {
135+
labels = append(labels, opt.Label)
136+
}
137+
if opt.Key == selectedKey && opt.Label != "" {
138+
selected = opt.Label
139+
}
140+
}
141+
142+
switch len(labels) {
143+
case 1:
144+
firstLine = fmt.Sprintf("%s (%s)", firstLine, labels[0])
145+
default:
146+
if len(labels) > 1 {
147+
firstLine = fmt.Sprintf("%s [%s]", firstLine, strings.Join(labels, "/"))
148+
}
149+
}
150+
151+
return fmt.Sprintf("%s %s", firstLine, selected)
152+
}
153+
125154
func (a App) View() string {
126155
var sb strings.Builder
127156
sb.WriteString(a.header.View())
@@ -133,7 +162,7 @@ func (a App) View() string {
133162
}
134163
if promptView := a.inputPrompt.View(); promptView != "" {
135164
sb.WriteString(" ")
136-
sb.WriteString(promptView)
165+
sb.WriteString(strings.ReplaceAll(promptView, "\n", "\n "))
137166
sb.WriteString("\n")
138167
}
139168
return sb.String()

internal/ui/app_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,74 @@ func TestAppCtrlCCancelsPendingInput(t *testing.T) {
142142
t.Fatalf("expected context canceled error, got %v", app.Err())
143143
}
144144
}
145+
146+
func TestAppEnterPrefersExplicitEnterOption(t *testing.T) {
147+
t.Parallel()
148+
149+
app := NewApp("dev", nil)
150+
responseCh := make(chan output.InputResponse, 1)
151+
152+
model, _ := app.Update(output.UserInputRequestEvent{
153+
Prompt: "Open browser now?",
154+
Options: []output.InputOption{
155+
{Key: "y", Label: "Y"},
156+
{Key: "n", Label: "n"},
157+
{Key: "enter", Label: "Press ENTER when complete"},
158+
},
159+
ResponseCh: responseCh,
160+
})
161+
app = model.(App)
162+
163+
model, cmd := app.Update(tea.KeyMsg{Type: tea.KeyEnter})
164+
app = model.(App)
165+
if cmd == nil {
166+
t.Fatal("expected response command")
167+
}
168+
cmd()
169+
170+
select {
171+
case resp := <-responseCh:
172+
if resp.SelectedKey != "enter" {
173+
t.Fatalf("expected enter key, got %q", resp.SelectedKey)
174+
}
175+
case <-time.After(time.Second):
176+
t.Fatal("timed out waiting for response on channel")
177+
}
178+
179+
if app.inputPrompt.Visible() {
180+
t.Fatal("expected input prompt to be hidden after response")
181+
}
182+
}
183+
184+
func TestAppEnterDoesNothingWithoutExplicitEnterOption(t *testing.T) {
185+
t.Parallel()
186+
187+
app := NewApp("dev", nil)
188+
responseCh := make(chan output.InputResponse, 1)
189+
190+
model, _ := app.Update(output.UserInputRequestEvent{
191+
Prompt: "Open browser now?",
192+
Options: []output.InputOption{
193+
{Key: "y", Label: "Y"},
194+
{Key: "n", Label: "n"},
195+
},
196+
ResponseCh: responseCh,
197+
})
198+
app = model.(App)
199+
200+
model, cmd := app.Update(tea.KeyMsg{Type: tea.KeyEnter})
201+
app = model.(App)
202+
if cmd != nil {
203+
t.Fatal("expected no response command when enter is not an explicit option")
204+
}
205+
206+
select {
207+
case resp := <-responseCh:
208+
t.Fatalf("expected no response, got %+v", resp)
209+
case <-time.After(200 * time.Millisecond):
210+
}
211+
212+
if !app.inputPrompt.Visible() {
213+
t.Fatal("expected input prompt to remain visible")
214+
}
215+
}

internal/ui/components/input_prompt.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,29 @@ func (p InputPrompt) View() string {
3737
if !p.visible {
3838
return ""
3939
}
40-
text := p.prompt
41-
switch len(p.options) {
40+
41+
lines := strings.Split(p.prompt, "\n")
42+
firstLine := lines[0]
43+
rest := lines[1:]
44+
labels := make([]string, 0, len(p.options))
45+
for _, opt := range p.options {
46+
if opt.Label != "" {
47+
labels = append(labels, opt.Label)
48+
}
49+
}
50+
51+
switch len(labels) {
4252
case 1:
43-
text += " (" + p.options[0].Label + ")"
53+
firstLine += " (" + labels[0] + ")"
4454
default:
45-
labels := make([]string, len(p.options))
46-
for i, opt := range p.options {
47-
labels[i] = opt.Label
55+
if len(labels) > 1 {
56+
firstLine += " [" + strings.Join(labels, "/") + "]"
4857
}
49-
text += " [" + strings.Join(labels, "/") + "]"
5058
}
51-
return styles.Message.Render(text)
59+
60+
if len(rest) == 0 {
61+
return styles.Message.Render(firstLine)
62+
}
63+
64+
return styles.Message.Render(strings.Join(append([]string{firstLine}, rest...), "\n"))
5265
}

0 commit comments

Comments
 (0)