okami.mom/albedo

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

dawntui: login on listFrame/listView, removal through the shared confirm on ctrl+d479178e

main
3.3 KiB122 linesraw
1package tui
2
3import (
4	"slices"
5
6	"charm.land/bubbles/v2/textinput"
7	tea "charm.land/bubbletea/v2"
8)
9
10// loginPick is a list a login step chooses from: its items, filtered and
11// drawn by the shared listView like every other list screen.
12type loginPick struct {
13	items []PickerItem
14	listView
15}
16
17// newLoginPick lists items under title, with the row keyed initial highlighted.
18func newLoginPick(title string, items []PickerItem, initial string, width, height int) loginPick {
19	p := loginPick{items: items, listView: newListView("Search")}
20	p.setSize(width, height)
21	rows := make([]listEntry, len(items))
22	for i, item := range items {
23		rows[i] = listEntry{
24			key:     item.ID,
25			section: title,
26			name:    item.Label,
27			desc:    item.Detail,
28			detail:  func(w int) []string { return loginPane(item, w) },
29		}
30	}
31	p.setRows(rows)
32	if i := slices.IndexFunc(p.shown, func(s listShown) bool { return p.rows[s.row].key == initial }); i >= 0 {
33		p.Cursor = i
34	}
35	return p
36}
37
38// Highlighted is the item under the cursor.
39func (p loginPick) Highlighted() (PickerItem, bool) {
40	row, ok := p.highlighted()
41	if !ok {
42		return PickerItem{}, false
43	}
44	i := slices.IndexFunc(p.items, func(item PickerItem) bool { return item.ID == row.key })
45	if i < 0 {
46		return PickerItem{}, false
47	}
48	return p.items[i], true
49}
50
51// Init starts the cursor blinking in the search line.
52func (p loginPick) Init() tea.Cmd { return textinput.Blink }
53
54// loginPane is an item's detail: what it is, what it holds, and what enter
55// or a key would do with it.
56func loginPane(item PickerItem, width int) []string {
57	lines := paneTitle(item.Label, "", width)
58	if item.Detail != "" {
59		lines = append(lines, factRows("details", item.Detail, width)...)
60	}
61	if item.Note != "" {
62		lines = append(lines, "")
63		lines = append(lines, paneNote(item.Note, width)...)
64	}
65	return lines
66}
67
68// pickerFor returns the list the current step shows.
69func (m *LoginModel) pickerFor() *loginPick {
70	switch m.Step {
71	case StepProtocol:
72		return &m.ProtocolPicker
73	case StepModels, StepOAuthModels:
74		return &m.ModelPicker
75	}
76	return &m.ChoosePicker
77}
78
79// listing reports whether the step shows a list rather than a question. A
80// catalog still loading is a question until its models arrive.
81func (m LoginModel) listing() bool {
82	switch m.Step {
83	case StepChoose, StepAccountProfile, StepOAuthFlow, StepProtocol:
84		return true
85	case StepModels, StepOAuthModels:
86		return m.Catalog != nil
87	case StepOAuthFields:
88		return m.choiceField()
89	}
90	return false
91}
92
93// choiceField reports the sign-in field being asked is a list of choices.
94func (m LoginModel) choiceField() bool {
95	if m.Step != StepOAuthFields || m.LoginFieldIndex >= len(m.LoginFields) {
96		return false
97	}
98	field := m.LoginFields[m.LoginFieldIndex]
99	return field.Type == "choice" || field.Type == "boolean"
100}
101
102// pickUpdate routes a message to the list the step shows. Enter and esc
103// answer with the messages the step already handles; every other key moves
104// or types in the search.
105func (m *LoginModel) pickUpdate(msg tea.Msg) tea.Cmd {
106	p := m.pickerFor()
107	key, ok := msg.(tea.KeyPressMsg)
108	if !ok {
109		return p.update(msg)
110	}
111	switch key.String() {
112	case "enter":
113		item, found := p.Highlighted()
114		if !found || !m.listing() {
115			return nil
116		}
117		return func() tea.Msg { return PickerSelectMsg{ID: item.ID} }
118	case "esc", "ctrl+c":
119		return func() tea.Msg { return PickerCancelMsg{} }
120	}
121	return p.update(msg)
122}