@@ -3,7 +3,6 @@ package list
33import (
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
257264type 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
281296func (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
447458func (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
482496func (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
514541func (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.
548575func (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
576603func (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
585613func (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
624655func (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
670703func (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
712729func (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