- Spinner, gradient progress bar and the multi-row process view ported from SSU to bubbletea v2 (input off, no signal handler, command context); boxed tables through lipgloss/table - Prompts use huh fields and a single-select port of SSU's selector with its confirmation screen on an interactive terminal; line prompts off a terminal and under --no-ansi are unchanged (piped passwords still work) - -q hides decoration but keeps data and errors; -n returns defaults and Secret fails with a hint on a terminal stdin; --no-ansi and non-TTY runs print today's plain lines - Helpers: GlobalsOf, VerbosityOf, IsQuiet, Interactive, ConfirmProceed, Process, Current, Failed, PrintAbove, ItemProgress, ErrAborted - diary's styled console lines print above a running live view
141 lines
3.8 KiB
Go
141 lines
3.8 KiB
Go
package bonfire
|
|
|
|
// A single-select port of SSU's selector (internal/cli/tui selector.go,
|
|
// selector_keys.go and confirm.go) on bubbletea v2: arrow keys or k/j move,
|
|
// enter selects and opens SSU's confirmation screen, enter there confirms
|
|
// and esc goes back; esc or ctrl+c in the list cancels. Multi-select, the
|
|
// filter and the detail pane are not ported (D-30).
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"charm.land/bubbles/v2/key"
|
|
tea "charm.land/bubbletea/v2"
|
|
)
|
|
|
|
// selectorKeys are the selector's key bindings.
|
|
type selectorKeys struct {
|
|
Up key.Binding
|
|
Down key.Binding
|
|
Confirm key.Binding
|
|
Back key.Binding
|
|
Quit key.Binding
|
|
}
|
|
|
|
func defaultSelectorKeys() selectorKeys {
|
|
return selectorKeys{
|
|
Up: key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "up")),
|
|
Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")),
|
|
Confirm: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select")),
|
|
Back: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "back")),
|
|
Quit: key.NewBinding(key.WithKeys("esc", "ctrl+c"), key.WithHelp("esc", "cancel")),
|
|
}
|
|
}
|
|
|
|
// selectorModel picks one item from a list and asks for confirmation.
|
|
type selectorModel struct {
|
|
palette Palette
|
|
keys selectorKeys
|
|
title string
|
|
items []string
|
|
cursor int
|
|
height int
|
|
confirming bool
|
|
chosen int
|
|
cancelled bool
|
|
}
|
|
|
|
func newSelectorModel(p Palette, title string, items []string, def int) selectorModel {
|
|
if def < 0 || def >= len(items) {
|
|
def = 0
|
|
}
|
|
return selectorModel{palette: p, keys: defaultSelectorKeys(), title: title, items: items, cursor: def, chosen: -1}
|
|
}
|
|
|
|
func (m selectorModel) Init() tea.Cmd { return nil }
|
|
|
|
func (m selectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.WindowSizeMsg:
|
|
m.height = msg.Height
|
|
return m, nil
|
|
case tea.KeyPressMsg:
|
|
if msg.String() == "ctrl+c" {
|
|
m.cancelled = true
|
|
return m, tea.Quit
|
|
}
|
|
if m.confirming {
|
|
switch {
|
|
case key.Matches(msg, m.keys.Confirm):
|
|
m.chosen = m.cursor
|
|
return m, tea.Quit
|
|
case key.Matches(msg, m.keys.Back):
|
|
m.confirming = false
|
|
}
|
|
return m, nil
|
|
}
|
|
switch {
|
|
case key.Matches(msg, m.keys.Up):
|
|
if m.cursor > 0 {
|
|
m.cursor--
|
|
}
|
|
case key.Matches(msg, m.keys.Down):
|
|
if m.cursor < len(m.items)-1 {
|
|
m.cursor++
|
|
}
|
|
case key.Matches(msg, m.keys.Confirm):
|
|
if len(m.items) > 0 {
|
|
m.confirming = true
|
|
}
|
|
case key.Matches(msg, m.keys.Quit):
|
|
m.cancelled = true
|
|
return m, tea.Quit
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// visible is the range of items that fits the terminal around the cursor.
|
|
func (m selectorModel) visible() (int, int) {
|
|
rows := len(m.items)
|
|
if m.height > 0 {
|
|
rows = min(rows, max(m.height-4, 3))
|
|
}
|
|
start := 0
|
|
if m.cursor >= rows {
|
|
start = m.cursor - rows + 1
|
|
}
|
|
return start, min(start+rows, len(m.items))
|
|
}
|
|
|
|
func (m selectorModel) View() tea.View {
|
|
p := m.palette
|
|
var b strings.Builder
|
|
if m.chosen >= 0 || m.cancelled {
|
|
// Leave one summary line once the program ends.
|
|
b.WriteString(p.Current.Render(symPrompt) + " " + m.title)
|
|
if m.chosen >= 0 {
|
|
b.WriteString(" " + p.Success.Render(m.items[m.chosen]))
|
|
}
|
|
b.WriteString("\n")
|
|
return tea.NewView(b.String())
|
|
}
|
|
if m.confirming {
|
|
b.WriteString(p.Header.Render("Selected:") + "\n\n")
|
|
b.WriteString(" " + m.items[m.cursor] + "\n\n")
|
|
b.WriteString(p.Muted.Render("enter confirm • esc back"))
|
|
return tea.NewView(b.String())
|
|
}
|
|
b.WriteString(p.Current.Render(symPrompt) + " " + p.Header.Render(m.title) + "\n")
|
|
start, end := m.visible()
|
|
for i := start; i < end; i++ {
|
|
if i == m.cursor {
|
|
b.WriteString(p.Current.Render("> ") + m.items[i] + "\n")
|
|
continue
|
|
}
|
|
b.WriteString(" " + m.items[i] + "\n")
|
|
}
|
|
b.WriteString(p.Muted.Render("↑/k up • ↓/j down • enter select • esc cancel"))
|
|
return tea.NewView(b.String())
|
|
}
|