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