Skip to content

Commit 1d5da98

Browse files
author
Kilian Drechsler
committed
Added "Equals" handling and Error types
- added SetEquals methode to allow user to add a equals function so that after sorting the cursor is again at the same item. Changed Sort Methode accordingly and added some more related methodes - added Error typs for better handling of errors on user side - added outcommented example of SelectedPrefix - Outcommented Up and Down Methods since the are redundant with Move(-1/1). - Updated some Doc-strings - fixed bug within selected prefix - added jump cases to example for marking actions
1 parent 7e3043a commit 1d5da98

File tree

3 files changed

+153
-51
lines changed

3 files changed

+153
-51
lines changed

list/example/main.go

+45-15
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,25 @@ func main() {
6161
}
6262

6363
res := <-endResult
64-
if res != "" {
65-
fmt.Println(res)
66-
}
64+
// allways print a newline even on empty string result
65+
fmt.Println(res)
6766
}
6867

6968
// initialize sets up the model and returns it to the bubbletea runtime
7069
// as a function result, so it can later be handed over to the update and view functions.
7170
func initialize(lineList []fmt.Stringer, endResult chan<- string) func() (tea.Model, tea.Cmd) {
7271
l := list.NewModel()
7372
l.AddItems(lineList)
74-
// l.WrapPrefix = false // uncomment for fancy check (selected) box :-)
73+
// uncomment the following lines for fancy check (selected) box :-)
74+
// l.WrapPrefix = false
7575
// l.SelectedPrefix = " [x]"
7676
// l.UnSelectedPrefix = "[ ]"
7777

78+
// Since in this example we only use UNIQUE string items we can use a String Comparison for the equals methode
79+
// but be aware that different items in your case can have the same string -> false-positiv
80+
// Better: Assert back to your struct and test on something unique within it!
81+
l.SetEquals(func(first, second fmt.Stringer) bool { return first.String() == second.String() })
82+
7883
return func() (tea.Model, tea.Cmd) { return model{list: l, endResult: endResult}, nil }
7984
}
8085

@@ -127,32 +132,57 @@ func update(msg tea.Msg, mdl tea.Model) (tea.Model, tea.Cmd) {
127132
case "r":
128133
m.list.NumberRelative = !m.list.NumberRelative
129134
return m, nil
130-
}
131-
132-
// Enter prints the selected lines to StdOut
133-
if msg.Type == tea.KeyEnter {
135+
case "m":
136+
j := 1
137+
if m.jump != "" {
138+
j, _ = strconv.Atoi(m.jump)
139+
m.jump = ""
140+
}
141+
m.list.MarkSelected(j, true)
142+
return m, nil
143+
case "M":
144+
j := 1
145+
if m.jump != "" {
146+
j, _ = strconv.Atoi(m.jump)
147+
m.jump = ""
148+
}
149+
m.list.MarkSelected(j, false)
150+
return m, nil
151+
case " ":
152+
j := 1
153+
if m.jump != "" {
154+
j, _ = strconv.Atoi(m.jump)
155+
m.jump = ""
156+
}
157+
m.list.ToggleSelect(j)
158+
m.list.Move(1)
159+
return m, nil
160+
case "enter":
161+
// Enter prints the selected lines to StdOut
134162
var result bytes.Buffer
135163
for _, item := range m.list.GetSelected() {
136164
result.WriteString(item.String())
137165
result.WriteString("\n")
138166
}
139167
m.endResult <- result.String()
140168
return m, tea.Quit
169+
default:
170+
// resets jump buffer to prevent confusion
171+
m.jump = ""
172+
173+
// pipe all other commands to the update from the list
174+
list, newMsg := list.Update(msg, m.list)
175+
m.list = list
176+
return m, newMsg
141177
}
142178

143-
// pipe all other commands to the update from the list
144-
list, newMsg := list.Update(msg, m.list)
145-
m.list = list
146-
147-
return m, newMsg
148-
149179
case tea.WindowSizeMsg:
150180

151181
m.list.Width = msg.Width
152182
m.list.Height = msg.Height
153183

154184
if !m.ready {
155-
// Since this program is using the full size of the viewport we need
185+
// Since this program can use the full size of the viewport we need
156186
// to wait until we've received the window dimensions before we
157187
// can initialize the viewport. The initial dimensions come in
158188
// quickly, though asynchronously, which is why we wait for them

list/list.go

+107-35
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,27 @@ import (
1010
"strings"
1111
)
1212

13+
// NotFound gets return if the search does not yield a result
14+
type NotFound error
15+
16+
// OutOfBounds is return if and index is outside the list bounderys
17+
type OutOfBounds error
18+
19+
// MultipleMatches gets return if the search yield more result
20+
type MultipleMatches error
21+
22+
// ConfigError is return if there is a error with the configuration of the list Modul
23+
type ConfigError error
24+
1325
// Model is a bubbletea List of strings
1426
type Model struct {
1527
focus bool
1628

1729
listItems []item
18-
curIndex int // cursor
19-
visibleOffset int // begin of the visible lines
20-
less func(k, l string) bool // function used for sorting
30+
curIndex int // cursor
31+
visibleOffset int // begin of the visible lines
32+
less func(string, string) bool // function used for sorting
33+
equals func(fmt.Stringer, fmt.Stringer) bool // to be set from the user
2134

2235
CursorOffset int // offset or margin between the cursor and the viewport(visible) border
2336

@@ -32,7 +45,7 @@ type Model struct {
3245
SeperatorWrap string
3346
CurrentMarker string
3447

35-
WrapPrefix bool
48+
PrefixWrap bool
3649

3750
Number bool
3851
NumberRelative bool
@@ -128,9 +141,11 @@ func (m *Model) Lines() []string {
128141
}
129142
selected = strings.Repeat(" ", selectWidth-selWid) + selected
130143

131-
wrapPrePad := unselect
132-
if !m.WrapPrefix {
133-
wrapPrePad = strings.Repeat(" ", selectWidth)
144+
wrapSelectPad := strings.Repeat(" ", selectWidth)
145+
wrapUnSelePad := strings.Repeat(" ", selectWidth)
146+
if m.PrefixWrap {
147+
wrapSelectPad = strings.Repeat(" ", selectWidth-selWid) + selected
148+
wrapUnSelePad = strings.Repeat(" ", selectWidth-tmpWid) + unselect
134149
}
135150

136151
unselect = strings.Repeat(" ", selectWidth-tmpWid) + unselect
@@ -195,9 +210,11 @@ out:
195210
selString := unselect
196211
style := m.LineStyle
197212

213+
wrapPrePad := wrapUnSelePad
198214
if item.selected {
199215
style = m.SelectedStyle
200216
selString = selected
217+
wrapPrePad = wrapSelectPad
201218
}
202219

203220
// Current: handle highlighting of current item/first-line
@@ -260,6 +277,7 @@ out:
260277

261278
// lineNumber returns line number of the given index
262279
// and if relative is true the absolute difference to the cursor
280+
// or if on the cursor the absolute line number
263281
func lineNumber(relativ bool, curser, current int) int {
264282
if !relativ || curser == current {
265283
return current
@@ -296,7 +314,7 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
296314
return m, nil
297315
case " ":
298316
m.ToggleSelect(1)
299-
m.Down()
317+
m.Move(1)
300318
return m, nil
301319
case "g":
302320
m.Top()
@@ -334,10 +352,10 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
334352
case tea.MouseMsg:
335353
switch msg.Button {
336354
case tea.MouseWheelUp:
337-
m.Up()
355+
m.Move(-1)
338356

339357
case tea.MouseWheelDown:
340-
m.Down()
358+
m.Move(1)
341359
}
342360
}
343361
return m, nil
@@ -356,19 +374,19 @@ func (m *Model) AddItems(itemList []fmt.Stringer) {
356374

357375
// Down moves the "cursor" or current line down.
358376
// If the end is already reached err is not nil.
359-
func (m *Model) Down() error {
360-
return m.Move(1)
361-
}
377+
//func (m *Model) Down() error {
378+
// return m.Move(1)
379+
//}
362380

363381
// Up moves the "cursor" or current line up.
364382
// If the start is already reached, err is not nil.
365-
func (m *Model) Up() error {
366-
return m.Move(-1)
367-
}
383+
//func (m *Model) Up() error {
384+
// return m.Move(-1)
385+
//}
368386

369387
// Move moves the cursor by amount, does nothing if amount is 0
370-
// and returns error != nil if amount go's beyond list borders
371-
// or if the CursorOffset is greater than half of the display height
388+
// and returns OutOfBounds error if amount go's beyond list borders
389+
// or if the CursorOffset is greater than half of the display height returns ConfigError
372390
func (m *Model) Move(amount int) error {
373391
// do nothing
374392
if amount == 0 {
@@ -380,12 +398,12 @@ func (m *Model) Move(amount int) error {
380398
height := m.Height
381399
if curOff >= height/2 {
382400
curOff = 0
383-
err = fmt.Errorf("cursor offset must be less than halfe of the display height: setting it to zero")
401+
err = ConfigError(fmt.Errorf("cursor offset must be less than halfe of the display height: setting it to zero"))
384402
}
385403

386404
target := m.curIndex + amount
387405
if !m.CheckWithinBorder(target) {
388-
return fmt.Errorf("Cant move outside the list: %d", target)
406+
return OutOfBounds(fmt.Errorf("Cant move outside the list: %d", target))
389407
}
390408
// move visible part of list if Cursor is going beyond border.
391409
lowerBorder := height + visOff - curOff
@@ -414,6 +432,7 @@ func (m *Model) Move(amount int) error {
414432
}
415433

416434
// NewModel returns a Model with some save/sane defaults
435+
// design to transfer as much internal information to the user
417436
func NewModel() Model {
418437
p := termenv.ColorProfile()
419438
selStyle := termenv.Style{}.Background(p.Color("#ff0000"))
@@ -427,7 +446,8 @@ func NewModel() Model {
427446
CursorOffset: 5,
428447

429448
// Wrap lines to have no loss of information
430-
Wrap: true,
449+
Wrap: true,
450+
PrefixWrap: true,
431451

432452
// Make clear where a item begins and where it ends
433453
Seperator: "╭",
@@ -467,7 +487,7 @@ func (m *Model) ToggleSelect(amount int) error {
467487
cur := m.curIndex
468488
target := cur + amount - direction
469489
if !m.CheckWithinBorder(target) {
470-
return fmt.Errorf("Cant go beyond list borders: %d", target)
490+
return OutOfBounds(fmt.Errorf("Cant go beyond list borders: %d", target))
471491
}
472492
for c := 0; c < amount*direction; c++ {
473493
m.listItems[cur+c].selected = !m.listItems[cur+c].selected
@@ -479,8 +499,8 @@ func (m *Model) ToggleSelect(amount int) error {
479499

480500
// MarkSelected selects or unselects depending on 'mark'
481501
// amount = 0 changes the current item but does not move the cursor
482-
// if amount would be outside the list error is not nil
483-
// else all items till but excluding the end cursor position
502+
// if amount would be outside the list error is from type OutOfBounds
503+
// else all items till but excluding the end cursor position gets (un-)marked
484504
func (m *Model) MarkSelected(amount int, mark bool) error {
485505
cur := m.curIndex
486506
if amount == 0 {
@@ -494,7 +514,7 @@ func (m *Model) MarkSelected(amount int, mark bool) error {
494514

495515
target := cur + amount - direction
496516
if !m.CheckWithinBorder(target) {
497-
return fmt.Errorf("Cant go beyond list borders: %d", target)
517+
return OutOfBounds(fmt.Errorf("Cant go beyond list borders: %d", target))
498518
}
499519
for c := 0; c < amount*direction; c++ {
500520
m.listItems[cur+c].selected = mark
@@ -546,7 +566,6 @@ func (m *Model) GetSelected() []fmt.Stringer {
546566
}
547567

548568
// Less is a Proxy to the less function, set from the user.
549-
// Swap is used to fulfill the Sort-interface
550569
func (m *Model) Less(i, j int) bool {
551570
return m.less(m.listItems[i].value.String(), m.listItems[j].value.String())
552571
}
@@ -566,11 +585,26 @@ func (m *Model) SetLess(less func(string, string) bool) {
566585
m.less = less
567586
}
568587

569-
// Sort sorts the listitems according to the set less function
570-
// The current Item will maybe change!
588+
// Sort sorts the list items according to the set less-function
589+
// If there is no Equals-function set (with SetEquals), the current Item will maybe change!
571590
// Since the index of the current pointer does not change
572591
func (m *Model) Sort() {
592+
equ := m.equals
593+
var tmp item
594+
if equ != nil {
595+
tmp = m.listItems[m.curIndex]
596+
}
573597
sort.Sort(m)
598+
if equ == nil {
599+
return
600+
}
601+
for i, item := range m.listItems {
602+
if is := equ(item.value, tmp.value); is {
603+
m.curIndex = i
604+
break // Stop when first (and hopefully only one) is found
605+
}
606+
}
607+
574608
}
575609

576610
// MoveItem moves the current item by amount to the end
@@ -585,7 +619,7 @@ func (m *Model) MoveItem(amount int) error {
585619
cur := m.curIndex
586620
target := cur + amount
587621
if !m.CheckWithinBorder(target) {
588-
return fmt.Errorf("Cant move outside the list: %d", target)
622+
return OutOfBounds(fmt.Errorf("Cant move outside the list: %d", target))
589623
}
590624
m.Swap(cur, target)
591625
m.curIndex = target
@@ -601,12 +635,6 @@ func (m *Model) CheckWithinBorder(index int) bool {
601635
return true
602636
}
603637

604-
// AddDataItem adds a Item with the given interface{} value added to the List item
605-
// So that when sorting, the connection between the string and the interfave{} value stays.
606-
//func (m *Model) AddDataItem(content string, data interface{}) {
607-
// m.listItems = append(m.listItems, item{content: content, userValue: data})
608-
//}
609-
610638
// Focus sets the list Model focus so it accepts key input and responds to them
611639
func (m *Model) Focus() {
612640
m.focus = true
@@ -621,3 +649,47 @@ func (m *Model) UnFocus() {
621649
func (m *Model) Focused() bool {
622650
return m.focus
623651
}
652+
653+
// SetEquals sets the internal equals methode used if provided to set the cursor again on the same item after sorting
654+
func (m *Model) SetEquals(equ func(first, second fmt.Stringer) bool) {
655+
m.equals = equ
656+
}
657+
658+
// GetEquals returns the internal equals methode
659+
// used to set the curser after sorting on the same item again
660+
func (m *Model) GetEquals() func(first, second fmt.Stringer) bool {
661+
return m.equals
662+
}
663+
664+
// GetIndex returns NotFound error if the Equals Methode is not set (SetEquals)
665+
// or multiple items match the returns MultipleMatches error
666+
// else it returns the index of the found found item
667+
func (m *Model) GetIndex(toSearch fmt.Stringer) (int, error) {
668+
if m.equals == nil {
669+
return -1, NotFound(fmt.Errorf("no equals function provided. Use SetEquals to set it"))
670+
}
671+
tmpList := m.listItems
672+
matchList := make([]chan bool, len(tmpList))
673+
equ := m.equals
674+
675+
for i, item := range tmpList {
676+
resChan := make(chan bool)
677+
matchList[i] = resChan
678+
go func(f, s fmt.Stringer, equ func(fmt.Stringer, fmt.Stringer) bool, res chan<- bool) {
679+
res <- equ(f, s)
680+
}(item.value, toSearch, equ, resChan)
681+
}
682+
683+
var c, lastIndex int
684+
for i, resChan := range matchList {
685+
if <-resChan {
686+
c++
687+
lastIndex = i
688+
}
689+
}
690+
if c > 1 {
691+
return -c, MultipleMatches(fmt.Errorf("The provided equals function yields multiple matches betwen one and other fmt.Stringer's"))
692+
}
693+
return lastIndex, nil
694+
695+
}

0 commit comments

Comments
 (0)