package main import ( "fmt" "io" "strings" ) type item struct{ ID, Label, Detail string } type selection struct { items []item visible []int // Indexes into items; never the identity returned to a caller. cursor int } func newSelection(items []item) selection { s := selection{items: append([]item(nil), items...)} s.filter("") return s } func (s *selection) current() (item, bool) { if s.cursor < 0 || s.cursor >= len(s.visible) { return item{}, false } return s.items[s.visible[s.cursor]], true } func (s *selection) filter(query string) { old, hadOld := s.current() s.visible = nil query = strings.ToLower(query) for i, row := range s.items { if strings.Contains(strings.ToLower(row.Label), query) { s.visible = append(s.visible, i) } } s.cursor = 0 if hadOld { for i, index := range s.visible { if s.items[index].ID == old.ID { s.cursor = i break } } } } func (s *selection) move(delta int) { s.cursor = min(max(0, s.cursor+delta), max(0, len(s.visible)-1)) } type outcome struct { Accepted, Interrupted bool ID string } // finish is called only after Bubble Tea has released the terminal. // Demo policy: 0 accepted, 1 cancelled, 130 keyboard interrupt, 2 failure. func finish(w io.Writer, result outcome) (int, error) { if result.Interrupted { return 130, nil } if !result.Accepted { return 1, nil } if result.ID == "" { return 2, fmt.Errorf("accepted result has no ID") } if _, err := fmt.Fprintln(w, result.ID); err != nil { return 2, err } return 0, nil }