1// Package choicebar is a small question asked on the context line and2// answered with a key, the way terminal programs confirm things, e.g.3// "You have unsaved changes. Enter Save and quit D Discard Esc Cancel".4// It takes the keyboard like an overlay but draws no box, and knows the5// UI only through Host.6package choicebar78import (9 "strings"1011 tea "charm.land/bubbletea/v2"12 "github.com/charmbracelet/x/ansi"1314 "src.rest/cat/012/internal/ui/overlay"15 "src.rest/cat/012/internal/ui/theme"16)1718// Host is what a choice bar needs of the UI.19type Host interface {20 Theme() *theme.Theme21 // Close closes the open overlay.22 Close()23 // RecordAnswer tells a macro recording the key chosen, for the24 // command that asked; Esc cancels it.25 RecordAnswer(answer string, cancelled bool)26}2728// Choice is one answer: a key as tea reports it ("enter", "d"), its29// label, and what choosing it does, after the bar has closed.30type Choice struct {31 Key string32 Label string33 Run func() tea.Cmd34}3536// Bar is the question and its choices.37type Bar struct {38 h Host39 Msg string40 Warn bool // the message is a warning, e.g. about losing work41 Desc string // more on the status line, when the question needs it42 Choices []Choice43}4445// New returns a bar asking msg.46func New(h Host, msg string, warn bool, desc string, choices []Choice) *Bar {47 return &Bar{h: h, Msg: msg, Warn: warn, Desc: desc, Choices: choices}48}4950func (b *Bar) Indicator() string { return "MENU" }51func (b *Bar) Layout() []overlay.Box { return nil }52func (b *Bar) Status() (string, string) { return b.Desc, "" }5354func (b *Bar) Key(k tea.KeyPressMsg) tea.Cmd {55 key := strings.ToLower(k.String())56 for i, ch := range b.Choices {57 if key == ch.Key {58 return b.Choose(i)59 }60 }61 return nil62}6364// Find is the index of the choice with key or label answer, ignoring65// case, or -1.66func (b *Bar) Find(answer string) int {67 for i, ch := range b.Choices {68 if strings.EqualFold(ch.Key, answer) || strings.EqualFold(ch.Label, answer) {69 return i70 }71 }72 return -173}7475// Choose closes the bar and runs choice i.76func (b *Bar) Choose(i int) tea.Cmd {77 ch := b.Choices[i]78 b.h.Close()79 cmd := ch.Run()80 b.h.RecordAnswer(ch.Key, ch.Key == "esc")81 return cmd82}8384// Mouse runs a choice when its key chip or label is clicked. A click85// anywhere else cancels, like Esc.86func (b *Bar) Mouse(e overlay.MouseEvent) tea.Cmd {87 if e.Kind != overlay.MousePress {88 return nil89 }90 if e.Y == overlay.ContextLine {91 x := ansi.StringWidth(b.prefix())92 for i, ch := range b.Choices {93 w := ansi.StringWidth(b.item(ch))94 if e.X >= x && e.X < x+w {95 return b.Choose(i)96 }97 x += w + len(gap)98 }99 }100 b.h.Close()101 return nil102}103104const gap = " "105106func (b *Bar) prefix() string {107 if b.Warn {108 return b.h.Theme().Warning.Render(b.Msg) + gap109 }110 return b.Msg + gap111}112113func (b *Bar) item(ch Choice) string {114 return b.h.Theme().Chip(theme.KeyLabel(ch.Key)) + " " + ch.Label115}116117// ContextLine renders the bar for the context line.118func (b *Bar) ContextLine() (string, string) {119 items := make([]string, len(b.Choices))120 for i, ch := range b.Choices {121 items[i] = b.item(ch)122 }123 return b.prefix() + strings.Join(items, gap), ""124}