1package list 2 3import ( 4 "fmt" 5 "reflect" 6 "strings" 7) 8 9// Searcher is a base function signature that is used inside select when activating the search mode. 10// If defined, it is called on each items of the select and should return a boolean for whether or not 11// the item fits the searched term. 12type Searcher func(input string, index int) bool 13 14// NotFound is an index returned when no item was selected. This could 15// happen due to a search without results. 16const NotFound = -1 17 18// List holds a collection of items that can be displayed with an N number of 19// visible items. The list can be moved up, down by one item of time or an 20// entire page (ie: visible size). It keeps track of the current selected item. 21type List struct { 22 items []*interface{} 23 scope []*interface{} 24 cursor int // cursor holds the index of the current selected item 25 size int // size is the number of visible options 26 start int 27 Searcher Searcher 28} 29 30// New creates and initializes a list of searchable items. The items attribute must be a slice type with a 31// size greater than 0. Error will be returned if those two conditions are not met. 32func New(items interface{}, size int) (*List, error) { 33 if size < 1 { 34 return nil, fmt.Errorf("list size %d must be greater than 0", size) 35 } 36 37 if items == nil || reflect.TypeOf(items).Kind() != reflect.Slice { 38 return nil, fmt.Errorf("items %v is not a slice", items) 39 } 40 41 slice := reflect.ValueOf(items) 42 values := make([]*interface{}, slice.Len()) 43 44 for i := range values { 45 item := slice.Index(i).Interface() 46 values[i] = &item 47 } 48 49 return &List{size: size, items: values, scope: values}, nil 50} 51 52// Prev moves the visible list back one item. If the selected item is out of 53// view, the new select item becomes the last visible item. If the list is 54// already at the top, nothing happens. 55func (l *List) Prev() { 56 if l.cursor > 0 { 57 l.cursor-- 58 } 59 60 if l.start > l.cursor { 61 l.start = l.cursor 62 } 63} 64 65// Search allows the list to be filtered by a given term. The list must 66// implement the searcher function signature for this functionality to work. 67func (l *List) Search(term string) { 68 term = strings.Trim(term, " ") 69 l.cursor = 0 70 l.start = 0 71 l.search(term) 72} 73 74// CancelSearch stops the current search and returns the list to its 75// original order. 76func (l *List) CancelSearch() { 77 l.cursor = 0 78 l.start = 0 79 l.scope = l.items 80} 81 82func (l *List) search(term string) { 83 var scope []*interface{} 84 85 for i, item := range l.items { 86 if l.Searcher(term, i) { 87 scope = append(scope, item) 88 } 89 } 90 91 l.scope = scope 92} 93 94// Start returns the current render start position of the list. 95func (l *List) Start() int { 96 return l.start 97} 98 99// SetStart sets the current scroll position. Values out of bounds will be 100// clamped. 101func (l *List) SetStart(i int) { 102 if i < 0 { 103 i = 0 104 } 105 if i > l.cursor { 106 l.start = l.cursor 107 } else { 108 l.start = i 109 } 110} 111 112// SetCursor sets the position of the cursor in the list. Values out of bounds 113// will be clamped. 114func (l *List) SetCursor(i int) { 115 max := len(l.scope) - 1 116 if i >= max { 117 i = max 118 } 119 if i < 0 { 120 i = 0 121 } 122 l.cursor = i 123 124 if l.start > l.cursor { 125 l.start = l.cursor 126 } else if l.start+l.size <= l.cursor { 127 l.start = l.cursor - l.size + 1 128 } 129} 130 131// Next moves the visible list forward one item. If the selected item is out of 132// view, the new select item becomes the first visible item. If the list is 133// already at the bottom, nothing happens. 134func (l *List) Next() { 135 max := len(l.scope) - 1 136 137 if l.cursor < max { 138 l.cursor++ 139 } 140 141 if l.start+l.size <= l.cursor { 142 l.start = l.cursor - l.size + 1 143 } 144} 145 146// PageUp moves the visible list backward by x items. Where x is the size of the 147// visible items on the list. The selected item becomes the first visible item. 148// If the list is already at the bottom, the selected item becomes the last 149// visible item. 150func (l *List) PageUp() { 151 start := l.start - l.size 152 if start < 0 { 153 l.start = 0 154 } else { 155 l.start = start 156 } 157 158 cursor := l.start 159 160 if cursor < l.cursor { 161 l.cursor = cursor 162 } 163} 164 165// PageDown moves the visible list forward by x items. Where x is the size of 166// the visible items on the list. The selected item becomes the first visible 167// item. 168func (l *List) PageDown() { 169 start := l.start + l.size 170 max := len(l.scope) - l.size 171 172 switch { 173 case len(l.scope) < l.size: 174 l.start = 0 175 case start > max: 176 l.start = max 177 default: 178 l.start = start 179 } 180 181 cursor := l.start 182 183 if cursor == l.cursor { 184 l.cursor = len(l.scope) - 1 185 } else if cursor > l.cursor { 186 l.cursor = cursor 187 } 188} 189 190// CanPageDown returns whether a list can still PageDown(). 191func (l *List) CanPageDown() bool { 192 max := len(l.scope) 193 return l.start+l.size < max 194} 195 196// CanPageUp returns whether a list can still PageUp(). 197func (l *List) CanPageUp() bool { 198 return l.start > 0 199} 200 201// Index returns the index of the item currently selected inside the searched list. If no item is selected, 202// the NotFound (-1) index is returned. 203func (l *List) Index() int { 204 selected := l.scope[l.cursor] 205 206 for i, item := range l.items { 207 if item == selected { 208 return i 209 } 210 } 211 212 return NotFound 213} 214 215// Items returns a slice equal to the size of the list with the current visible 216// items and the index of the active item in this list. 217func (l *List) Items() ([]interface{}, int) { 218 var result []interface{} 219 max := len(l.scope) 220 end := l.start + l.size 221 222 if end > max { 223 end = max 224 } 225 226 active := NotFound 227 228 for i, j := l.start, 0; i < end; i, j = i+1, j+1 { 229 if l.cursor == i { 230 active = j 231 } 232 233 result = append(result, *l.scope[i]) 234 } 235 236 return result, active 237} 238