Skip to content

Commit 81eb395

Browse files
Added more test and removed there by found bugs
- Added more tests, to cover the Update function and other public functions - Added IsSelected to check directly selected state of index/item - fixed bug within MarkSelected, ToggleSelect, MoveItem, GetCursorIndex to return OutOfBounds error when list has no items - Changed keepVisibleWrap to set wrong targets to the nearest listend - fixed bug within Top and MarkSelected to return errors - added SetCursor Methode to directly move to Index - changed structs less field to function with fmt.Stringer argument and changed Sort and SetLess -method accordingly - added default case to Update in example - commented test-keys out - Move lineNumber to prefixer.go - Changed Copy to copy also private Fields
1 parent 7857dc9 commit 81eb395

File tree

4 files changed

+413
-74
lines changed

4 files changed

+413
-74
lines changed

list/example/main.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
tea "github.com/charmbracelet/bubbletea"
88
"os"
99
"strconv"
10-
"strings"
1110
)
1211

1312
type model struct {
@@ -175,13 +174,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
175174
}
176175
m.endResult <- result.String()
177176
return m, tea.Quit
178-
case "t":
179-
m.lastViews = append(m.lastViews, m.View())
180-
return m, nil
181-
case "T":
182-
f, _ := os.OpenFile("test_cases.txt", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
183-
f.WriteString(strings.Join(m.lastViews, "\n##########################\n"))
184-
return m, tea.Quit
177+
// case "t":
178+
// m.lastViews = append(m.lastViews, m.View())
179+
// return m, nil
180+
// case "T":
181+
// f, _ := os.OpenFile("test_cases.txt", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
182+
// f.WriteString(strings.Join(m.lastViews, "\n##########################\n"))
183+
// return m, tea.Quit
185184
default:
186185
// resets jump buffer to prevent confusion
187186
m.jump = ""
@@ -209,6 +208,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
209208
m.ready = true
210209
}
211210
return m, nil
211+
212+
default:
213+
// pipe all other commands to the update from the list
214+
l, newMsg := m.list.Update(msg)
215+
list, _ := l.(list.Model)
216+
m.list = list
217+
return m, newMsg
212218
}
213-
return m, nil
214219
}

list/list.go

Lines changed: 75 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package list
33
import (
44
"fmt"
55
tea "github.com/charmbracelet/bubbletea"
6-
"github.com/jinzhu/copier"
76
"github.com/muesli/reflow/ansi"
87
"github.com/muesli/termenv"
98
"sort"
@@ -16,7 +15,7 @@ type Model struct {
1615

1716
listItems []item
1817

19-
less func(string, string) bool // function used for sorting
18+
less func(fmt.Stringer, fmt.Stringer) bool // function used for sorting
2019
equals func(fmt.Stringer, fmt.Stringer) bool // used after sorting, to be set from the user
2120

2221
CursorOffset int // offset or margin between the cursor and the viewport(visible) border
@@ -51,8 +50,8 @@ func NewModel() Model {
5150
// Wrap lines to have no loss of information
5251
Wrap: true,
5352

54-
less: func(k, l string) bool {
55-
return k < l
53+
less: func(k, l fmt.Stringer) bool {
54+
return k.String() < l.String()
5655
},
5756

5857
SelectedStyle: selStyle,
@@ -175,38 +174,39 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
175174

176175
switch msg := msg.(type) {
177176
case tea.KeyMsg:
178-
// Ctrl+c exits
177+
// Quit
179178
if msg.Type == tea.KeyCtrlC {
180179
return m, tea.Quit
181180
}
182181
switch msg.String() {
183182
case "q":
184183
return m, tea.Quit
184+
185+
// Move
185186
case "down", "j":
186187
m.Move(1)
187188
return m, nil
188189
case "up", "k":
189190
m.Move(-1)
190191
return m, nil
191-
case " ":
192-
m.ToggleSelect(1)
193-
m.Move(1)
194-
return m, nil
195192
case "g":
196193
m.Top()
197194
return m, nil
198195
case "G":
199196
m.Bottom()
200197
return m, nil
201-
case "s":
202-
m.Sort()
203-
return m, nil
204198
case "+":
205199
m.MoveItem(-1)
206200
return m, nil
207201
case "-":
208202
m.MoveItem(1)
209203
return m, nil
204+
205+
// Select
206+
case " ":
207+
m.ToggleSelect(1)
208+
m.Move(1)
209+
return m, nil
210210
case "v": // inVert
211211
m.ToggleAllSelected()
212212
return m, nil
@@ -216,6 +216,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
216216
case "M": // mark False
217217
m.MarkSelected(1, false)
218218
return m, nil
219+
220+
// Order changing
221+
case "s":
222+
m.Sort()
223+
return m, nil
219224
}
220225

221226
case tea.WindowSizeMsg:
@@ -230,9 +235,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
230235
switch msg.Type {
231236
case tea.MouseWheelUp:
232237
m.Move(-1)
238+
return m, nil
233239

234240
case tea.MouseWheelDown:
235241
m.Move(1)
242+
return m, nil
236243
}
237244
}
238245
return m, nil
@@ -255,9 +262,9 @@ type NotFocused error
255262

256263
// ViewPos is used for holding the information about the View parameters
257264
type ViewPos struct {
258-
Cursor int
259265
ItemOffset int
260266
LineOffset int
267+
Cursor int
261268
}
262269

263270
// ScreenInfo holds all information about the screen Area
@@ -277,6 +284,14 @@ func (m *Model) Move(amount int) (int, error) {
277284
return newPos.Cursor, err
278285
}
279286

287+
// SetCursor set the cursor to the specified index if possible,
288+
// if not the nearest end of the list, will be used and OutOfBounds error is returned
289+
func (m *Model) SetCursor(target int) error {
290+
newPos, err := m.KeepVisible(target)
291+
m.viewPos = newPos
292+
return err
293+
}
294+
280295
// Top moves the cursor to the first line
281296
func (m *Model) Top() {
282297
m.viewPos.Cursor = 0
@@ -312,11 +327,11 @@ func (m *Model) KeepVisible(target int) (ViewPos, error) {
312327
}
313328

314329
if target == 0 {
315-
return ViewPos{}, nil
330+
return ViewPos{}, err
316331
}
317332

318333
if m.Wrap {
319-
return m.keepVisibleWrap(target)
334+
return m.keepVisibleWrap(target), err
320335
}
321336

322337
m.viewPos.LineOffset = 0
@@ -343,14 +358,15 @@ func (m *Model) KeepVisible(target int) (ViewPos, error) {
343358
return ViewPos{Cursor: target, ItemOffset: lowerOffset}, err
344359
}
345360

346-
func (m *Model) keepVisibleWrap(target int) (ViewPos, error) {
347-
348-
if !m.CheckWithinBorder(target) {
349-
return ViewPos{}, OutOfBounds(fmt.Errorf("can't move beyond list bonderys, with requested cursor position: %d", target))
361+
// keepVisibleWrap returns the new viewPos according to the requested target Cursor position
362+
// is target is outside the list return the nearest end
363+
func (m *Model) keepVisibleWrap(target int) ViewPos {
364+
if target <= 0 {
365+
return ViewPos{}
350366
}
351367

352-
if target == 0 {
353-
return ViewPos{}, nil
368+
if target >= m.Len() {
369+
target = m.Len() - 1
354370
}
355371

356372
direction := 1
@@ -400,32 +416,27 @@ func (m *Model) keepVisibleWrap(target int) (ViewPos, error) {
400416
lineCount[len(lineCount)-1].linesBefor < m.CursorOffset && // beyond upper border
401417
m.viewPos.ItemOffset <= 0 && m.viewPos.LineOffset <= 0 { // but allready at beginning of list
402418

403-
return ViewPos{Cursor: target}, nil
419+
return ViewPos{Cursor: target}
404420
}
405421

406422
var lastOffset, lineOffset int
407423
for _, count := range lineCount {
408424
lastOffset = count.listIndex // Visible Offset
409-
// can't Move beyond list end, setting offsets accordingly
410-
if target >= len(m.listItems)-1 && count.linesBefor > lowerBorder {
411-
lineOffset = count.linesBefor - lowerBorder
412-
return ViewPos{ItemOffset: lastOffset, LineOffset: lineOffset, Cursor: len(m.listItems) - 1}, nil
413-
}
414425
// infront upper border -> Move up
415426
if direction < 0 && !upper && count.linesBefor > upperBorder {
416427
lineOffset = count.linesBefor - upperBorder
417-
return ViewPos{ItemOffset: lastOffset, LineOffset: lineOffset, Cursor: target}, nil
428+
return ViewPos{ItemOffset: lastOffset, LineOffset: lineOffset, Cursor: target}
418429
}
419430
// beyond lower border -> Moving Down
420431
if direction >= 0 && lower && count.linesBefor >= lowerBorder {
421432
lastOffset = count.listIndex // Visible Offset
422433
lineOffset = count.linesBefor - lowerBorder
423-
return ViewPos{ItemOffset: lastOffset, LineOffset: lineOffset, Cursor: target}, nil
434+
return ViewPos{ItemOffset: lastOffset, LineOffset: lineOffset, Cursor: target}
424435
}
425436
}
426437

427438
// Within bounds: only change cursor
428-
return ViewPos{ItemOffset: m.viewPos.ItemOffset, LineOffset: m.viewPos.LineOffset, Cursor: target}, nil
439+
return ViewPos{ItemOffset: m.viewPos.ItemOffset, LineOffset: m.viewPos.LineOffset, Cursor: target}
429440
}
430441

431442
// AddItems adds the given Items to the list Model
@@ -445,6 +456,9 @@ func (m *Model) AddItems(itemList []fmt.Stringer) {
445456
// else if amount is not 0 toggles selected amount items
446457
// excluding the item on which the cursor would land
447458
func (m *Model) ToggleSelect(amount int) error {
459+
if m.Len() == 0 {
460+
return OutOfBounds(fmt.Errorf("No Items"))
461+
}
448462
if amount == 0 {
449463
m.listItems[m.viewPos.Cursor].selected = !m.listItems[m.viewPos.Cursor].selected
450464
}
@@ -466,7 +480,7 @@ func (m *Model) ToggleSelect(amount int) error {
466480
start = 0
467481
}
468482
// mark last item when trying to go beyond list
469-
if cur+amount >= len(m.listItems) {
483+
if cur+amount >= m.Len() {
470484
end++
471485
}
472486
for c := start; c < end; c++ {
@@ -480,6 +494,9 @@ func (m *Model) ToggleSelect(amount int) error {
480494
// if amount would be outside the list error is from type OutOfBounds
481495
// else all items till but excluding the end cursor position gets (un-)marked
482496
func (m *Model) MarkSelected(amount int, mark bool) error {
497+
if m.Len() == 0 {
498+
return OutOfBounds(fmt.Errorf("No Items within list"))
499+
}
483500
cur := m.viewPos.Cursor
484501
if amount == 0 {
485502
m.listItems[cur].selected = mark
@@ -498,8 +515,8 @@ func (m *Model) MarkSelected(amount int, mark bool) error {
498515
m.listItems[cur+c].selected = mark
499516
}
500517
m.viewPos.Cursor = target
501-
m.Move(direction)
502-
return nil
518+
_, err := m.Move(direction)
519+
return err
503520
}
504521

505522
// ToggleAllSelected inverts the select state of ALL items
@@ -509,6 +526,16 @@ func (m *Model) ToggleAllSelected() {
509526
}
510527
}
511528

529+
// IsSelected returns true if the given Item is selected
530+
// false otherwise. If the requested index is outside the list
531+
// error is not nil.
532+
func (m *Model) IsSelected(index int) (bool, error) {
533+
if !m.CheckWithinBorder(index) {
534+
return false, OutOfBounds(fmt.Errorf("index: '%d' is outside the list", index))
535+
}
536+
return m.listItems[index].selected, nil
537+
}
538+
512539
// GetSelected returns you a list of all items
513540
// that are selected in current (displayed) order
514541
func (m *Model) GetSelected() []fmt.Stringer {
@@ -546,7 +573,7 @@ func (m *Model) Sort() {
546573

547574
// Less is a Proxy to the less function, set from the user.
548575
func (m *Model) Less(i, j int) bool {
549-
return m.less(m.listItems[i].value.String(), m.listItems[j].value.String())
576+
return m.less(m.listItems[i].value, m.listItems[j].value)
550577
}
551578

552579
// Swap swaps the items position within the list
@@ -562,7 +589,7 @@ func (m *Model) Len() int {
562589
}
563590

564591
// SetLess sets the internal less function used for sorting the list items
565-
func (m *Model) SetLess(less func(string, string) bool) {
592+
func (m *Model) SetLess(less func(a, b fmt.Stringer) bool) {
566593
m.less = less
567594
}
568595

@@ -574,6 +601,7 @@ func (m *Model) SetEquals(equ func(first, second fmt.Stringer) bool) {
574601
// GetEquals returns the internal equals methode
575602
// used to set the curser after sorting on the same item again
576603
func (m *Model) GetEquals() func(first, second fmt.Stringer) bool {
604+
// TODO remove this function?
577605
return m.equals
578606
}
579607

@@ -583,6 +611,9 @@ func (m *Model) GetEquals() func(first, second fmt.Stringer) bool {
583611
// MoveItem(0) safely does nothing
584612
// and a amount that would result outside the list returns a error != nil
585613
func (m *Model) MoveItem(amount int) error {
614+
if m.Len() == 0 {
615+
return OutOfBounds(fmt.Errorf("can't get MoveItem on empty list"))
616+
}
586617
if amount == 0 {
587618
return nil
588619
}
@@ -620,7 +651,7 @@ func (m *Model) Focused() bool {
620651
}
621652

622653
// GetIndex returns NotFound error if the Equals Methode is not set (SetEquals)
623-
// else it returns the index of the found item
654+
// else it returns the index of the first found item
624655
func (m *Model) GetIndex(toSearch fmt.Stringer) (int, error) {
625656
if m.equals == nil {
626657
return -1, NotFound(fmt.Errorf("no equals function provided. Use SetEquals to set it"))
@@ -645,6 +676,7 @@ func (m *Model) GetIndex(toSearch fmt.Stringer) (int, error) {
645676
}
646677
}
647678
if c > 1 {
679+
// TODO performance: trust User and remove check for multiple matches?
648680
return -c, MultipleMatches(fmt.Errorf("The provided equals function yields multiple matches betwen one and other fmt.Stringer's"))
649681
}
650682
return lastIndex, nil
@@ -667,14 +699,14 @@ func (m *Model) UpdateSelectedItems(updater func(fmt.Stringer) fmt.Stringer) {
667699
}
668700

669701
// GetCursorIndex returns current cursor position within the List
702+
// and also NotFocused error if the Model is not focused
670703
func (m *Model) GetCursorIndex() (int, error) {
704+
if m.Len() == 0 {
705+
return 0, OutOfBounds(fmt.Errorf("No Items"))
706+
}
671707
if !m.focus {
672708
return m.viewPos.Cursor, NotFocused(fmt.Errorf("Model is not focused"))
673709
}
674-
if m.CheckWithinBorder(m.viewPos.Cursor) {
675-
return m.viewPos.Cursor, OutOfBounds(fmt.Errorf("Cursor is out auf bounds"))
676-
}
677-
// TODO handel not focused case
678710
return m.viewPos.Cursor, nil
679711
}
680712

@@ -693,24 +725,9 @@ func (m *Model) GetAllItems() []fmt.Stringer {
693725
//func (m *Model) MoveByLine(amount) (ViewPos, error) {
694726
//}
695727

696-
// lineNumber returns line number of the given index
697-
// and if relative is true the absolute difference to the cursor
698-
// or if on the cursor the absolute line number
699-
func lineNumber(relativ bool, curser, current int) int {
700-
if !relativ || curser == current {
701-
return current
702-
}
703-
704-
diff := curser - current
705-
if diff < 0 {
706-
diff *= -1
707-
}
708-
return diff
709-
}
710-
711728
// Copy returns a deep copy of the list-model
712729
func (m *Model) Copy() *Model {
713-
copiedModel := Model{}
714-
copier.Copy(&copiedModel, &m)
715-
return &copiedModel
730+
copiedModel := &Model{}
731+
*copiedModel = *m
732+
return copiedModel
716733
}

0 commit comments

Comments
 (0)