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