55 "fmt"
66 "github.com/charmbracelet/bubbles/viewport"
77 tea "github.com/charmbracelet/bubbletea"
8- runewidth "github.com/mattn/go-runewidth"
9- "github.com/muesli/reflow/wordwrap"
108 "github.com/muesli/reflow/ansi"
9+ "github.com/muesli/reflow/wordwrap"
1110 "github.com/muesli/termenv"
1211 "sort"
1312 "strings"
@@ -18,23 +17,23 @@ type Model struct {
1817 focus bool
1918
2019 listItems []item
21- curIndex int
22- visibleOffset int
23- lineCurserOffset int
24- less func (k , l string ) bool
20+ curIndex int // curser
21+ visibleOffset int // begin of the visible lines
22+ lineCurserOffset int // offset or margin between the cursor and the viewport(visible) border
23+ less func (k , l string ) bool // function used for sorting
2524
2625 jump int // maybe buffer for jumping multiple lines
2726
2827 Viewport viewport.Model
2928 Wrap bool
3029
31- SelectedPrefix string
32- Seperator string
33- SeperatorWrap string
34- CurrentMarker string
30+ SelectedPrefix string
31+ Seperator string
32+ SeperatorWrap string
33+ CurrentMarker string
3534
36- Number bool
37- NumberRelative bool
35+ Number bool
36+ NumberRelative bool
3837
3938 LineForeGroundStyle termenv.Style
4039 LineBackGroundStyle termenv.Style
@@ -70,27 +69,49 @@ func View(model tea.Model) string {
7069 }
7170
7271 // check visible area
73- height := m .Viewport .Height
72+ height := m .Viewport .Height - 1 // TODO question: why -1, otherwise firstline gets cut of
7473 width := m .Viewport .Width
74+ offset := m .visibleOffset
7575 if height * width <= 0 {
7676 panic ("Can't display with zero width or hight of Viewport" )
7777 }
7878
79-
8079 // Get max seperator width
8180 widthItem := ansi .PrintableRuneWidth (m .Seperator )
8281 widthWrap := ansi .PrintableRuneWidth (m .SeperatorWrap )
8382
84-
8583 // Find max width
8684 sepWidth := widthItem
8785 if widthWrap > sepWidth {
8886 sepWidth = widthWrap
8987 }
9088
89+ // get widest *displayed* number, for padding
90+ numWidth := len (fmt .Sprintf ("%d" , len (m .listItems )- 1 ))
91+ localMaxWidth := len (fmt .Sprintf ("%d" , offset + height - 1 ))
92+ if localMaxWidth < numWidth {
93+ numWidth = localMaxWidth
94+ }
95+
96+ // pad all prefixes to the same width for easy exchange
97+ prefix := m .SelectedPrefix
98+ preWidth := ansi .PrintableRuneWidth (prefix )
99+ prepad := strings .Repeat (" " , preWidth )
100+
101+ // pad all seperators to the same width for easy exchange
102+ sepItem := strings .Repeat (" " , sepWidth - widthItem ) + m .Seperator
103+ sepWrap := strings .Repeat (" " , sepWidth - widthWrap ) + m .SeperatorWrap
104+
105+ // pad right of prefix, with lenght of current pointer
106+ suffix := m .CurrentMarker
107+ sufWidth := ansi .PrintableRuneWidth (suffix )
108+ sufpad := strings .Repeat (" " , sufWidth )
109+
110+ // Get the hole prefix width
111+ holePrefixWidth := numWidth + preWidth + sepWidth + sufWidth
112+
91113 // Get actual content width
92- numWidth := len (m .listItems )
93- contentWidth := width - (numWidth + sepWidth )
114+ contentWidth := width - holePrefixWidth
94115
95116 // Check if there is space for the content left
96117 if contentWidth <= 0 {
@@ -100,73 +121,54 @@ func View(model tea.Model) string {
100121 // renew wrap of all items TODO check if to slow
101122 for i := range m .listItems {
102123 m .listItems [i ] = m .listItems [i ].genVisLines (contentWidth )
103-
104124 }
105125
106-
107- // pad all prefixes to the same width for easy exchange
108- prefix := m .SelectedPrefix
109- prepad := strings .Repeat (" " , ansi .PrintableRuneWidth (m .SelectedPrefix ))
110-
111- // pad all seperators to the same width for easy exchange
112- sepItem := strings .Repeat (" " , sepWidth - widthItem )+ m .Seperator
113- sepWrap := strings .Repeat (" " , sepWidth - widthWrap )+ m .SeperatorWrap
114-
115- // pad right of prefix, with lenght of current pointer
116- suffix := m .CurrentMarker
117- sufpad := strings .Repeat (" " , ansi .PrintableRuneWidth (suffix ))
118-
119126 var visLines int
120127 var holeString bytes.Buffer
121128out:
122129 // Handle list items, start at first visible and go till end of list or visible (break)
123- for index := m . visibleOffset ; index < len (m .listItems ); index ++ {
130+ for index := offset ; index < len (m .listItems ); index ++ {
124131 if index >= len (m .listItems ) || index < 0 {
132+ // TODO log error
125133 break
126134 }
127135
128136 item := m .listItems [index ]
129- if item .wrapedLenght = = 0 {
137+ if item .wrapedLenght < = 0 {
130138 panic ("cant display item with no visible content" )
131139 }
132140
133-
134- var linePrefix , wrapPrefix string
135-
136- // if a number is set, prepend firstline with number and both with enough spaceses
141+ // if a number is set, prepend firstline with number and both with enough spaces
137142 firstPad := strings .Repeat (" " , numWidth )
138143 var wrapPad string
139144 if m .Number {
140- lineNum := lineNumber (m .NumberRelative , m .curIndex , m . visibleOffset + index )
145+ lineNum := lineNumber (m .NumberRelative , m .curIndex , index )
141146 number := fmt .Sprintf ("%d" , lineNum )
142- // since diggets are only singel bytes len is sufficent:
147+ // since diggets are only singel bytes, len is sufficent:
143148 firstPad = strings .Repeat (" " , numWidth - len (number )) + number
144149 // pad wraped lines
145150 wrapPad = strings .Repeat (" " , numWidth )
146151 }
147152
148-
149153 // Selecting: handel highlighting and prefixing of selected lines
150- selString := prepad // assume not selected
154+ selString := prepad // assume not selected
151155 style := termenv .String () // create empty style
152156
153157 if item .selected {
154158 style = m .SelectedBackGroundStyle // fill style
155- selString = prefix // change if selected
159+ selString = prefix // change if selected
156160 }
157161
158-
159162 // Current: handel highlighting of current item/first-line
160163 curPad := sufpad
161- if index + m . visibleOffset == m .curIndex {
164+ if index == m .curIndex {
162165 style = style .Reverse ()
163166 curPad = suffix
164167 }
165168
166169 // join all prefixes
167- linePrefix = strings .Join ([]string {firstPad , linePrefix , selString , sepItem , curPad }, "" )
168- wrapPrefix = strings .Join ([]string {wrapPad , linePrefix , selString , sepWrap , sufpad }, "" )
169-
170+ linePrefix := strings .Join ([]string {firstPad , selString , sepItem , curPad }, "" )
171+ wrapPrefix := strings .Join ([]string {wrapPad , selString , sepWrap , sufpad }, "" ) // dont prefix wrap lines with CurrentMarker (suffix)
170172
171173 // join pad and first line content
172174 // NOTE linebreak is not added here because it would mess with the highlighting
@@ -177,16 +179,16 @@ out:
177179 holeString .WriteString ("\n " )
178180 visLines ++
179181
180- // Dont write wraped lines if not set
181- if ! m .Wrap || item .wrapedLenght <= 1 {
182- continue
183- }
184-
185182 // Only write lines that are visible
186183 if visLines >= height {
187184 break out
188185 }
189186
187+ // Dont write wraped lines if not set
188+ if ! m .Wrap || item .wrapedLenght <= 1 {
189+ continue
190+ }
191+
190192 // Write wraped lines
191193 for _ , line := range item .wrapedLines [1 :] {
192194 // Pad left of line
@@ -241,13 +243,11 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
241243 return m , nil
242244 }
243245
244-
245246 case tea.WindowSizeMsg :
246247
247248 m .Viewport .Width = msg .Width
248249 m .Viewport .Height = msg .Height
249250
250-
251251 // Because we're using the viewport's default update function (with pager-
252252 // style navigation) it's important that the viewport's update function:
253253 //
@@ -284,8 +284,8 @@ func (m *Model) Down() error {
284284 m .curIndex ++
285285 // move visible part of list if Curser is going beyond border.
286286 lowerBorder := m .Viewport .Height + m .visibleOffset - m .lineCurserOffset
287- if m .curIndex >= lowerBorder {
288- m .visibleOffset ++
287+ if m .curIndex > lowerBorder {
288+ m .visibleOffset = m . curIndex - ( m . Viewport . Height - m . lineCurserOffset )
289289 }
290290 return nil
291291}
@@ -301,7 +301,7 @@ func (m *Model) Up() error {
301301 // move visible part of list if Curser is going beyond border.
302302 upperBorder := m .visibleOffset + m .lineCurserOffset
303303 if m .visibleOffset > 0 && m .curIndex <= upperBorder {
304- m .visibleOffset --
304+ m .visibleOffset = m . curIndex - m . lineCurserOffset
305305 }
306306 return nil
307307}
@@ -320,11 +320,12 @@ func NewModel() Model {
320320
321321 Wrap : true ,
322322
323- Seperator : "╭" ,
324- SeperatorWrap : "│" ,
325- CurrentMarker : ">" ,
326- SelectedPrefix : "*" ,
327- Number : true ,
323+ Seperator : "╭" ,
324+ SeperatorWrap : "│" ,
325+ CurrentMarker : ">" ,
326+ SelectedPrefix : "*" ,
327+ Number : true ,
328+
328329 less : func (k , l string ) bool {
329330 return k < l
330331 },
@@ -341,9 +342,9 @@ func (m *Model) Top() {
341342
342343// Bottom moves the cursor to the last line
343344func (m * Model ) Bottom () {
344- end := len (m .listItems )- 1
345+ end := len (m .listItems ) - 1
345346 m .curIndex = end
346- maxVisItems := m .Viewport .Height - m .lineCurserOffset
347+ maxVisItems := m .Viewport .Height - m .lineCurserOffset
347348 var visLines , smallestVisIndex int
348349 for c := end ; visLines < maxVisItems ; c -- {
349350 if c < 0 {
@@ -355,21 +356,8 @@ func (m *Model) Bottom() {
355356 m .visibleOffset = smallestVisIndex
356357}
357358
358- // maxRuneWidth returns the maximal lenght of occupied space
359- // frome the given strings
360- func maxRuneWidth (words ... string ) int {
361- var max int
362- for _ , w := range words {
363- width := runewidth .StringWidth (w )
364- if width > max {
365- max = width
366- }
367- }
368- return max
369- }
370-
371- // GetSelected returns you a orderd list of all items
372- // that are selected
359+ // GetSelected returns you a list of all items
360+ // that are selected in current (displayed) order
373361func (m * Model ) GetSelected () []string {
374362 var selected []string
375363 for _ , item := range m .listItems {
@@ -406,12 +394,14 @@ func (m *Model) Sort() {
406394 sort .Sort (m )
407395}
408396
397+ // lineNumber returns line number of the given index
398+ // and if relative is true the absolute difference to the curser
409399func lineNumber (relativ bool , curser , current int ) int {
410400 if ! relativ || curser == current {
411401 return current
412402 }
413403
414- diff := curser - current
404+ diff := curser - current
415405 if diff < 0 {
416406 diff *= - 1
417407 }
0 commit comments