okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

dawnadd colab extension47735ec

main
6.1 KiB200 linesraw
1package tui
2
3import (
4	"albedo/cli/internal/config"
5	"cmp"
6
7	tea "charm.land/bubbletea/v2"
8	"github.com/charmbracelet/x/ansi"
9)
10
11func (m LoginModel) openBrowserCmd(urlStr string) tea.Cmd {
12	return func() tea.Msg {
13		if m.openBrowser != nil {
14			m.openBrowser(urlStr)
15		}
16		return nil
17	}
18}
19
20// inputWidth leaves the frame's margin and the field's label their room.
21func (m LoginModel) inputWidth() int {
22	return max(8, m.Width-10-ansi.StringWidth(m.fieldLabel()))
23}
24
25// fieldLabel is what the text input is asked for.
26func (m LoginModel) fieldLabel() string {
27	if m.Step == StepOAuth {
28		return "callback URL or code"
29	}
30	label, _ := m.question()
31	return label
32}
33
34// promptInput re-arms the text input for the next question and reports the
35// blink command that shows its cursor. The placeholder is left as the
36// previous step left it.
37func (m *LoginModel) promptInput(value string, secret bool) tea.Cmd {
38	m.TextInput.SetWidth(m.inputWidth())
39	return ask(&m.TextInput, value, secret)
40}
41
42// fail shows an error on the current step and keeps waiting for input.
43func (m LoginModel) fail(err string) (LoginModel, tea.Cmd) {
44	m.Error = err
45	return m, nil
46}
47
48func (m *LoginModel) SetSize(width, height int) {
49	m.Width, m.Height = width, height
50	m.TextInput.SetWidth(m.inputWidth())
51	m.TextInput.SetValue(m.TextInput.Value())
52	for _, p := range []*loginPick{&m.ChoosePicker, &m.ProtocolPicker, &m.ModelPicker} {
53		p.setSize(width, height)
54	}
55}
56
57// View is the login step in the list frame: a list for a choice, the
58// question's input in the search line for a text step, and the step's text
59// above it. Errors and progress sit in the footer.
60func (m LoginModel) View() string {
61	width := cmp.Or(m.Width, 80)
62	title := brand("albedo") + " " + DefaultStyles.Muted.Render("/login")
63	right := DefaultStyles.Faint.Render(m.Name)
64	if m.Step == StepChoose {
65		right = DefaultStyles.Faint.Render(config.HomeDir())
66	}
67	footer := m.footer(width)
68	if m.listing() {
69		lv := m.pickerFor().listView
70		lv.Empty = "nothing to choose"
71		return lv.frame(title, right, footer).view(m.Width, m.Height)
72	}
73	f := listFrame{
74		title:  title,
75		right:  right,
76		filter: promptLead() + DefaultStyles.Muted.Render(m.fieldLabel()+"  ") + m.TextInput.View(),
77		list:   func(w, _ int) []string { return m.body(w) },
78		footer: footer,
79	}
80	return f.view(m.Width, m.Height)
81}
82
83// body is the text above a question: what it asks, what it takes, and what
84// a sign-in or save is doing.
85func (m LoginModel) body(width int) []string {
86	switch m.Step {
87	case StepRemove:
88		kind := m.Removing.Kind
89		return []string{DefaultStyles.Bold.Render(m.Removing.Label), DefaultStyles.Faint.Render(kind)}
90	case StepSaving:
91		state := "saving provider…"
92		if m.Removing.Kind != "" {
93			state = "removing " + m.Removing.Kind + "…"
94		}
95		return []string{DefaultStyles.Faint.Render(state)}
96	case StepOAuth:
97		lines := []string{DefaultStyles.Muted.Render(cmp.Or(m.Status, "starting sign-in…"))}
98		if m.LoginInstructions != "" {
99			lines = append(lines, paneNote(m.LoginInstructions, width)...)
100		}
101		if m.SignInURL != "" {
102			lines = append(lines, "", DefaultStyles.Faint.Render(ansi.Hardwrap(m.SignInURL, width, true)))
103		}
104		lines = append(lines, "")
105		return append(lines, paneNote("browser sign-in finishes on its own; paste the callback URL or code if it stalls", width)...)
106	case StepModels, StepOAuthModels:
107		return []string{DefaultStyles.Faint.Render("loading models…")}
108	}
109	_, notes := m.question()
110	var lines []string
111	for _, note := range notes {
112		lines = append(lines, paneNote(note, width)...)
113	}
114	return lines
115}
116
117// question is the label a text step asks for, and what it says beside it.
118func (m LoginModel) question() (string, []string) {
119	switch m.Step {
120	case StepName:
121		var notes []string
122		for _, login := range m.SignIns {
123			notes = append(notes, "use "+login.Provider+" to sign in")
124		}
125		return "provider name", notes
126	case StepBaseURL:
127		if m.optionalKey() {
128			return "API base URL", []string{"blank uses your daemon's environment"}
129		}
130		return "API base URL", nil
131	case StepAPIKey:
132		return "API key", nil
133	case StepProject:
134		return "Google Cloud project ID", nil
135	case StepLocation:
136		return "Vertex location", nil
137	case StepModel:
138		return "model ID", nil
139	case StepOAuthFields:
140		if m.LoginFieldIndex < len(m.LoginFields) {
141			field := m.LoginFields[m.LoginFieldIndex]
142			return field.Label, []string{field.Description}
143		}
144	}
145	return "", nil
146}
147
148// hints are the keys for the step, most important first.
149func (m LoginModel) hints() []hint {
150	switch {
151	case m.Step == StepSaving:
152		return nil
153	case (m.Step == StepModels || m.Step == StepOAuthModels) && !m.listing():
154		return []hint{{"esc", "back"}}
155	case m.listing():
156		hints := []hint{{"↑↓", "move"}, {"enter", m.enterVerb()}}
157		if item, ok := m.ChoosePicker.Highlighted(); ok && m.Step == StepChoose {
158			if _, removable := m.removalFor(item.ID); removable {
159				hints = append(hints, hint{"ctrl+d", "remove"})
160			}
161		}
162		return append(hints, hint{"esc", "back"})
163	case m.Step == StepOAuth:
164		return []hint{{"enter", "submit code"}, {"esc", "cancel"}}
165	case m.Step == StepAPIKey && m.Draft.APIKey != "":
166		return []hint{{"enter", "keeps the saved key"}, {"esc", "cancel"}}
167	}
168	return []hint{{"enter", "continue"}, {"esc", "cancel"}}
169}
170
171// enterVerb says what enter does on the list the step shows.
172func (m LoginModel) enterVerb() string {
173	switch m.Step {
174	case StepAccountProfile:
175		return "use"
176	case StepOAuthFlow:
177		return "sign in"
178	case StepModels, StepOAuthModels:
179		return "use model"
180	}
181	return "choose"
182}
183
184// footer is the keys, with an error on a row of its own above them so the
185// keys keep their labels.
186func (m LoginModel) footer(width int) string {
187	if m.Step == StepRemove {
188		return m.confirm.footer(width, m.Error)
189	}
190	if m.Error != "" {
191		return " " + DefaultStyles.Error.Render(ansi.Truncate(m.Error, max(1, width-2), "…")) + "\n" + footerLine(width, m.hints(), "", false)
192	}
193	status := ""
194	if m.CatalogNote != "" && (m.Step == StepModels || m.Step == StepOAuthModels) {
195		status = DefaultStyles.Faint.Render(m.CatalogNote)
196	} else if m.Status != "" && m.Step == StepChoose {
197		status = DefaultStyles.Faint.Render(m.Status)
198	}
199	return footerLine(width, m.hints(), status, false)
200}