summaryrefslogtreecommitdiffstats
path: root/src/terminal.go
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2024-05-20 01:33:33 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2024-05-20 08:55:37 +0900
commit025808559d10b28c846480679a68d87cb218b1f5 (patch)
tree9e570f39a22b63c5699ead01fb9344882348be68 /src/terminal.go
parent5b204c54f9d16accdf66bb24477e9eff4fc3a21a (diff)
Implement multi-line display of multi-line itemsdevel
Diffstat (limited to 'src/terminal.go')
-rw-r--r--src/terminal.go529
1 files changed, 353 insertions, 176 deletions
diff --git a/src/terminal.go b/src/terminal.go
index 01960552..8c2f3096 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -148,14 +148,25 @@ type eachLine struct {
}
type itemLine struct {
- offset int
- current bool
- selected bool
- label string
- queryLen int
- width int
- bar bool
- result Result
+ firstLine int
+ cy int
+ current bool
+ selected bool
+ label string
+ queryLen int
+ width int
+ hasBar bool
+ result Result
+ empty bool
+ other bool
+}
+
+func (t *Terminal) markEmptyLine(line int) {
+ t.prevLines[line] = itemLine{firstLine: line, empty: true}
+}
+
+func (t *Terminal) markOtherLine(line int) {
+ t.prevLines[line] = itemLine{firstLine: line, other: true}
}
type fitpad struct {
@@ -163,8 +174,6 @@ type fitpad struct {
pad int
}
-var emptyLine = itemLine{}
-
type labelPrinter func(tui.Window, int)
type StatusItem struct {
@@ -224,6 +233,7 @@ type Terminal struct {
yanked []rune
input []rune
multi int
+ multiLine bool
sort bool
toggleSort bool
track trackOption
@@ -739,6 +749,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
yanked: []rune{},
input: input,
multi: opts.Multi,
+ multiLine: opts.ReadZero && opts.MultiLine,
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
track: opts.Track,
@@ -1009,8 +1020,9 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
}
}
output := func() {
+ line := t.promptLine()
t.printHighlighted(
- Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false)
+ Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false, line, line, nil, nil)
}
_, promptLen := t.processTabs([]rune(trimmed), 0)
@@ -1031,22 +1043,45 @@ func (t *Terminal) noSeparatorLine() bool {
return noSeparatorLine(t.infoStyle, t.separatorLen > 0)
}
-func getScrollbar(total int, height int, offset int) (int, int) {
- if total == 0 || total <= height {
+func getScrollbar(perLine int, total int, height int, offset int) (int, int) {
+ if total == 0 || total*perLine <= height {
return 0, 0
}
- barLength := util.Max(1, height*height/total)
+ barLength := util.Max(1, height*height/(total*perLine))
var barStart int
if total == height {
barStart = 0
} else {
- barStart = (height - barLength) * offset / (total - height)
+ barStart = util.Min(height-barLength, (height*perLine-barLength)*offset/(total*perLine-height))
}
return barLength, barStart
}
+// Estimate the average number of lines per item. Instead of going through all
+// items, we only check a few items around the current cursor position.
+func (t *Terminal) avgNumLines() int {
+ if !t.multiLine {
+ return 1
+ }
+
+ maxItems := t.maxItems()
+ numLines := 0
+ count := 0
+ total := t.merger.Length()
+ offset := util.Max(0, util.Min(t.offset, total-maxItems-1))
+ for idx := 0; idx < maxItems && idx+offset < total; idx++ {
+ item := t.merger.Get(idx + offset)
+ numLines += item.item.text.NumLines(maxItems)
+ count++
+ }
+ if count == 0 {
+ return 1
+ }
+ return numLines / count
+}
+
func (t *Terminal) getScrollbar() (int, int) {
- return getScrollbar(t.merger.Length(), t.maxItems(), t.offset)
+ return getScrollbar(t.avgNumLines(), t.merger.Length(), t.maxItems(), t.offset)
}
// Input returns current query string
@@ -1622,7 +1657,6 @@ func (t *Terminal) placeCursor() {
}
func (t *Terminal) printPrompt() {
- t.move(t.promptLine(), 0, true)
t.prompt()
before, after := t.updatePromptOffset()
@@ -1645,6 +1679,10 @@ func (t *Terminal) trimMessage(message string, maxWidth int) string {
func (t *Terminal) printInfo() {
pos := 0
line := t.promptLine()
+ move := func(y int, x int, clear bool) {
+ t.move(y, x, clear)
+ t.markOtherLine(y)
+ }
printSpinner := func() {
if t.reading {
duration := int64(spinnerDuration)
@@ -1663,7 +1701,7 @@ func (t *Terminal) printInfo() {
str = string(trimmed)
width = maxWidth
}
- t.move(line, pos, t.separatorLen == 0)
+ move(line, pos, t.separatorLen == 0)
if t.reading {
t.window.CPrint(tui.ColSpinner, str)
} else {
@@ -1672,7 +1710,6 @@ func (t *Terminal) printInfo() {
pos += width
}
printSeparator := func(fillLength int, pad bool) {
- // --------_
if t.separatorLen > 0 {
t.separator(t.window, fillLength)
t.window.Print(" ")
@@ -1682,12 +1719,12 @@ func (t *Terminal) printInfo() {
}
switch t.infoStyle {
case infoDefault:
- t.move(line+1, 0, t.separatorLen == 0)
+ move(line+1, 0, t.separatorLen == 0)
printSpinner()
t.window.Print(" ") // Margin
pos = 2
case infoRight:
- t.move(line+1, 0, false)
+ move(line+1, 0, false)
case infoInlineRight:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
case infoInline:
@@ -1695,7 +1732,7 @@ func (t *Terminal) printInfo() {
printInfoPrefix()
case infoHidden:
if t.separatorLen > 0 {
- t.move(line+1, 0, false)
+ move(line+1, 0, false)
printSeparator(t.window.Width()-1, false)
}
return
@@ -1755,7 +1792,7 @@ func (t *Terminal) printInfo() {
if t.infoStyle == infoInlineRight {
if len(t.infoPrefix) == 0 {
- t.move(line, pos, false)
+ move(line, pos, false)
newPos := util.Max(pos, t.window.Width()-util.StringWidth(output)-3)
t.window.Print(strings.Repeat(" ", newPos-pos))
pos = newPos
@@ -1779,7 +1816,7 @@ func (t *Terminal) printInfo() {
if t.infoStyle == infoInlineRight {
if t.separatorLen > 0 {
- t.move(line+1, 0, false)
+ move(line+1, 0, false)
printSeparator(t.window.Width()-1, false)
}
return
@@ -1829,10 +1866,9 @@ func (t *Terminal) printHeader() {
text: util.ToChars([]byte(trimmed)),
colors: colors}
- t.move(line, 0, true)
- t.window.Print(" ")
t.printHighlighted(Result{item: item},
- tui.ColHeader, tui.ColHeader, false, false)
+ tui.ColHeader, tui.ColHeader, false, false, line, line,
+ func(int) { t.window.Print(" ") }, nil)
}
}
@@ -1840,53 +1876,66 @@ func (t *Terminal) printList() {
t.constrain()
barLength, barStart := t.getScrollbar()
- maxy := t.maxItems()
+ maxy := t.maxItems() - 1
count := t.merger.Length() - t.offset
- for j := 0; j < maxy; j++ {
- i := j
- if t.layout == layoutDefault {
- i = maxy - 1 - j
- }
- line := i + 2 + t.visibleHeaderLines()
- if t.noSeparatorLine() {
- line--
- }
- if i < count {
- t.printItem(t.merger.Get(i+t.offset), line, i, i == t.cy-t.offset, i >= barStart && i < barStart+barLength)
- } else if t.prevLines[i] != emptyLine || t.prevLines[i].offset != line {
- t.prevLines[i] = emptyLine
+
+ // Start line
+ startLine := 2 + t.visibleHeaderLines()
+ if t.noSeparatorLine() {
+ startLine--
+ }
+ maxy += startLine
+
+ barRange := [2]int{startLine + barStart, startLine + barStart + barLength}
+ for line, itemCount := startLine, 0; line <= maxy; line, itemCount = line+1, itemCount+1 {
+ if itemCount < count {
+ item := t.merger.Get(itemCount + t.offset)
+ line = t.printItem(item, line, maxy, itemCount, itemCount == t.cy-t.offset, barRange)
+ } else if !t.prevLines[line].empty {
t.move(line, 0, true)
+ t.markEmptyLine(line)
+ // If the screen is not filled with the list in non-multi-line mode,
+ // scrollbar is not visible at all. But in multi-line mode, we may need
+ // to redraw the scrollbar character at the end.
+ if t.multiLine {
+ t.prevLines[line].hasBar = t.printBar(line, true, barRange)
+ }
+ }
+ }
+}
+
+func (t *Terminal) printBar(lineNum int, forceRedraw bool, barRange [2]int) bool {
+ hasBar := lineNum >= barRange[0] && lineNum < barRange[1]
+ if len(t.scrollbar) > 0 && (hasBar != t.prevLines[lineNum].hasBar || forceRedraw) {
+ t.move(lineNum, t.window.Width()-1, true)
+ if hasBar {
+ t.window.CPrint(tui.ColScrollbar, t.scrollbar)
}
}
+ return hasBar
}
-func (t *Terminal) printItem(result Result, line int, i int, current bool, bar bool) {
+func (t *Terminal) printItem(result Result, line int, maxLine int, index int, current bool, barRange [2]int) int {
item := result.item
_, selected := t.selected[item.Index()]
label := ""
if t.jumping != jumpDisabled {
- if i < len(t.jumpLabels) {
+ if index < len(t.jumpLabels) {
// Striped
- current = i%2 == 0
- label = t.jumpLabels[i:i+1] + strings.Repeat(" ", t.pointerLen-1)
+ current = index%2 == 0
+ label = t.jumpLabels[index:index+1] + strings.Repeat(" ", t.pointerLen-1)
}
} else if current {
label = t.pointer
}
// Avoid unnecessary redraw
- newLine := itemLine{offset: line, current: current, selected: selected, label: label,
- result: result, queryLen: len(t.input), width: 0, bar: bar}
- prevLine := t.prevLines[i]
- forceRedraw := prevLine.offset != newLine.offset
- printBar := func() {
- if len(t.scrollbar) > 0 && (bar != prevLine.bar || forceRedraw) {
- t.prevLines[i].bar = bar
- t.move(line, t.window.Width()-1, true)
- if bar {
- t.window.CPrint(tui.ColScrollbar, t.scrollbar)
- }
- }
+ newLine := itemLine{firstLine: line, cy: index + t.offset, current: current, selected: selected, label: label,
+ result: result, queryLen: len(t.input), width: 0, hasBar: line >= barRange[0] && line < barRange[1]}
+ prevLine := t.prevLines[line]
+ forceRedraw := prevLine.other || prevLine.firstLine != newLine.firstLine
+ printBar := func(lineNum int, forceRedraw bool) bool {
+ return t.printBar(lineNum, forceRedraw, barRange)
}
if !forceRedraw &&
@@ -1895,33 +1944,64 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool, bar b
prevLine.label == newLine.label &&
prevLine.queryLen == newLine.queryLen &&
prevLine.result == newLine.result {
- printBar()
- return
+ t.prevLines[line].hasBar = printBar(line, false)
+ if !t.multiLine {
+ return line
+ }
+ return line + item.text.NumLines(maxLine-line+1) - 1
}
- t.move(line, 0, forceRedraw)
- if current {
- if len(label) == 0 {
- t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty)
+ maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
+ postTask := func(lineNum int, width int) {
+ if (current || selected) && t.highlightLine {
+ color := tui.ColSelected
+ if current {
+ color = tui.ColCurrent
+ }
+ fillSpaces := maxWidth - width
+ if fillSpaces > 0 {
+ t.window.CPrint(color, strings.Repeat(" ", fillSpaces))
+ }
+ newLine.width = maxWidth
} else {
- t.window.CPrint(tui.ColCurrentCursor, label)
+ fillSpaces := t.prevLines[lineNum].width - width
+ if fillSpaces > 0 {
+ t.window.Print(strings.Repeat(" ", fillSpaces))
+ }
+ newLine.width = width
}
- if selected {
- t.window.CPrint(tui.ColCurrentMarker, t.marker)
- } else {
- t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty)
+ // When width is 0, line is completely cleared. We need to redraw scrollbar
+ newLine.hasBar = printBar(lineNum, forceRedraw || width == 0)
+ t.prevLines[lineNum] = newLine
+ }
+
+ var finalLineNum int
+ if current {
+ preTask := func(lineOffset int) {
+ if len(label) == 0 {
+ t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty)
+ } else {
+ t.window.CPrint(tui.ColCurrentCursor, label)
+ }
+ if selected {
+ t.window.CPrint(tui.ColCurrentMarker, t.marker)
+ } else {
+ t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty)
+ }
}
- newLine.width = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true)
+ finalLineNum = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true, line, maxLine, preTask, postTask)
} else {
- if len(label) == 0 {
- t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty)
- } else {
- t.window.CPrint(tui.ColCursor, label)
- }
- if selected {
- t.window.CPrint(tui.ColMarker, t.marker)
- } else {
- t.window.Print(t.markerEmpty)
+ preTask := func(lineOffset int) {
+ if len(label) == 0 {
+ t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty)
+ } else {
+ t.window.CPrint(tui.ColCursor, label)
+ }
+ if selected {
+ t.window.CPrint(tui.ColMarker, t.marker)
+ } else {
+ t.window.Print(t.markerEmpty)
+ }
}
var base, match tui.ColorPair
if selected {
@@ -1931,27 +2011,9 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool, bar b
base = tui.ColNormal
match = tui.ColMatch
}
- newLine.width = t.printHighlighted(result, base, match, false, true)
+ finalLineNum = t.printHighlighted(result, base, match, false, true, line, maxLine, preTask, postTask)
}
- if (current || selected) && t.highlightLine {
- color := tui.ColSelected
- if current {
- color = tui.ColCurrent
- }
- maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
- fillSpaces := maxWidth - newLine.width
- newLine.width = maxWidth
- if fillSpaces > 0 {
- t.window.CPrint(color, strings.Repeat(" ", fillSpaces))
- }
- } else {
- fillSpaces := prevLine.width - newLine.width
- if fillSpaces > 0 {
- t.window.Print(strings.Repeat(" ", fillSpaces))
- }
- }
- printBar()
- t.prevLines[i] = newLine
+ return finalLineNum
}
func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) {
@@ -1992,12 +2054,9 @@ func (t *Terminal) overflow(runes []rune, max int) bool {
return t.displayWidthWithLimit(runes, 0, max) > max
}
-func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool) int {
+func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool, lineNum int, maxLineNum int, preTask func(int), postTask func(int, int)) int {
+ var displayWidth int
item := result.item
-
- // Overflow
- text := make([]rune, item.text.Length())
- copy(text, item.text.ToRunes())
matchOffsets := []Offset{}
var pos *[]int
if match && t.merger.pattern != nil {
@@ -2012,69 +2071,139 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
}
sort.Sort(ByOrder(charOffsets))
}
- var maxe int
- for _, offset := range charOffsets {
- maxe = util.Max(maxe, int(offset[1]))
+ allOffsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
+
+ from := 0
+ text := make([]rune, item.text.Length())
+ copy(text, item.text.ToRunes())
+
+ finalLineNum := lineNum
+ numItemLines := 1
+ cutoff := 0
+ if t.multiLine && t.layout == layoutDefault {
+ maxLines := maxLineNum - lineNum + 1
+ numItemLines = item.text.NumLines(maxLines)
+ // Cut off the upper lines in the 'default' layout
+ if !current && maxLines == numItemLines {
+ cutoff = item.text.NumLines(math.MaxInt32) - maxLines
+ }
}
+ for lineOffset := 0; from <= len(text) && (lineNum <= maxLineNum || maxLineNum == 0); lineOffset++ {
+ finalLineNum = lineNum
- offsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
- maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
- ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
- maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(text))
- displayWidth := t.displayWidthWithLimit(text, 0, maxWidth)
- if displayWidth > maxWidth {
- transformOffsets := func(diff int32, rightTrim bool) {
- for idx, offset := range offsets {
- b, e := offset.offset[0], offset.offset[1]
- el := int32(len(ellipsis))
- b += el - diff
- e += el - diff
- b = util.Max32(b, el)
- if rightTrim {
- e = util.Min32(e, int32(maxWidth-ellipsisWidth))
- }
- offsets[idx].offset[0] = b
- offsets[idx].offset[1] = util.Max32(b, e)
+ line := text[from:]
+ if t.multiLine {
+ for idx, r := range text[from:] {
+ if r == '\n' {
+ line = line[:idx]
+ break
+ }
}
}
- if t.hscroll {
- if t.keepRight && pos == nil {
- trimmed, diff := t.trimLeft(text, maxWidth-ellipsisWidth)
- transformOffsets(diff, false)
- text = append(ellipsis, trimmed...)
- } else if !t.overflow(text[:maxe], maxWidth-ellipsisWidth) {
- // Stri..
- text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
- text = append(text, ellipsis...)
+
+ offsets := []colorOffset{}
+ for _, offset := range allOffsets {
+ if offset.offset[0] >= int32(from) && offset.offset[1] <= int32(from+len(line)) {
+ offset.offset[0] -= int32(from)
+ offset.offset[1] -= int32(from)
+ offsets = append(offsets, offset)
} else {
- // Stri..
- rightTrim := false
- if t.overflow(text[maxe:], ellipsisWidth) {
- text = append(text[:maxe], ellipsis...)
- rightTrim = true
- }
- // ..ri..
- var diff int32
- text, diff = t.trimLeft(text, maxWidth-ellipsisWidth)
-
- // Transform offsets
- transformOffsets(diff, rightTrim)
- text = append(ellipsis, text...)
+ allOffsets = allOffsets[len(offsets):]
+ break
}
- } else {
- text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
- text = append(text, ellipsis...)
+ }
+
+ from += len(line) + 1
+
+ if cutoff > 0 {
+ cutoff--
+ lineOffset--
+ continue
+ }
+
+ var maxe int
+ for _, offset := range offsets {
+ if offset.match {
+ maxe = util.Max(maxe, int(offset.offset[1]))
+ }
+ }
+
+ actualLineNum := lineNum
+ if t.layout == layoutDefault {
+ actualLineNum = (lineNum - lineOffset) + (numItemLines - lineOffset) - 1
+ }
+ // NOTE: postTask is nil when using this function for printing prompt line or headers
+ t.move(actualLineNum, 0, postTask == nil || t.prevLines[actualLineNum].other)
+
+ if preTask != nil {
+ preTask(lineOffset)
+ }
+
+ maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
+ ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
+ maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(line))
+ displayWidth = t.displayWidthWithLimit(line, 0, maxWidth)
+ if displayWidth > maxWidth {
+ transformOffsets := func(diff int32, rightTrim bool) {
+ for idx, offset := range offsets {
+ b, e := offset.offset[0], offset.offset[1]
+ el := int32(len(ellipsis))
+ b += el - diff
+ e += el - diff
+ b = util.Max32(b, el)
+ if rightTrim {
+ e = util.Min32(e, int32(maxWidth-ellipsisWidth))
+ }
+ offsets[idx].offset[0] = b
+ offsets[idx].offset[1] = util.Max32(b, e)
+ }
+ }
+ if t.hscroll {
+ if t.keepRight && pos == nil {
+ trimmed, diff := t.trimLeft(line, maxWidth-ellipsisWidth)
+ transformOffsets(diff, false)
+ line = append(ellipsis, trimmed...)
+ } else if !t.overflow(line[:maxe], maxWidth-ellipsisWidth) {
+ // Stri..
+ line, _ = t.trimRight(line, maxWidth-ellipsisWidth)
+ line = append(line, ellipsis...)
+ } else {
+ // Stri..
+ rightTrim := false
+ if t.overflow(line[maxe:], ellipsisWidth) {
+ line = append(line[:maxe], ellipsis...)
+ rightTrim = true
+ }
+ // ..ri..
+ var diff int32
+ line, diff = t.trimLeft(line, maxWidth-ellipsisWidth)
+
+ // Transform offsets
+ transformOffsets(diff, rightTrim)
+ line = append(ellipsis, line...)
+ }
+ } else {
+ line, _ = t.trimRight(line, maxWidth-ellipsisWidth)
+ line = append(line, ellipsis...)
- for idx, offset := range offsets {
- offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
- offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
+ for idx, offset := range offsets {
+ offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
+ offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
+ }
}
+ displayWidth = t.displayWidthWithLimit(line, 0, displayWidth)
+ }
+
+ t.printColoredString(t.window, line, offsets, colBase)
+ if postTask != nil {
+ postTask(actualLineNum, displayWidth)
+ } else {
+ t.markOtherLine(actualLineNum)
}
- displayWidth = t.displayWidthWithLimit(text, 0, displayWidth)
+ lineNum += 1
}
- t.printColoredString(t.window, text, offsets, colBase)
- return displayWidth
+ return finalLineNum
}
func (t *Terminal) printColoredString(window tui.Window, text []rune, offsets []colorOffset, colBase tui.ColorPair) {
@@ -2172,7 +2301,7 @@ func (t *Terminal) renderPreviewArea(unchanged bool) {
}
effectiveHeight := height - headerLines
- barLength, barStart := getScrollbar(len(body), effectiveHeight, util.Min(len(body)-effectiveHeight, t.previewer.offset-headerLines))
+ barLength, barStart := getScrollbar(1, len(body), effectiveHeight, util.Min(len(body)-effectiveHeight, t.previewer.offset-headerLines))
t.renderPreviewScrollbar(headerLines, barLength, barStart)
}
@@ -4022,7 +4151,7 @@ func (t *Terminal) Loop() error {
if pbarDragging {
effectiveHeight := t.pwindow.Height() - headerLines
numLines := len(t.previewer.lines) - headerLines
- barLength, _ := getScrollbar(numLines, effectiveHeight, util.Min(numLines-effectiveHeight, t.previewer.offset-headerLines))
+ barLength, _ := getScrollbar(1, numLines, effectiveHeight, util.Min(numLines-effectiveHeight, t.previewer.offset-headerLines))
if barLength > 0 {
y := my - t.pwindow.Top() - headerLines - barLength/2
y = util.Constrain(y, 0, effectiveHeight-barLength)
@@ -4068,7 +4197,8 @@ func (t *Terminal) Loop() error {
total := t.merger.Length()
prevOffset := t.offset
// barStart = (maxItems - barLength) * t.offset / (total - maxItems)
- t.offset = int(math.Ceil(float64(newBarStart) * float64(total-maxItems) / float64(maxItems-barLength)))
+ perLine := t.avgNumLines()
+ t.offset = int(math.Ceil(float64(newBarStart) * float64(total*perLine-maxItems) / float64(maxItems*perLine-barLength)))
t.cy = t.offset + t.cy - prevOffset
req(reqList)
}
@@ -4076,11 +4206,18 @@ func (t *Terminal) Loop() error {
break
}
+ // There can be empty lines after the list in multi-line mode
+ prevLine := t.prevLines[my]
+ if prevLine.empty {
+ break
+ }
+
// Double-click on an item
+ cy := prevLine.cy
if me.Double && mx < t.window.Width()-1 {
// Double-click
if my >= min {
- if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
+ if t.vset(cy) && t.cy < t.merger.Length() {
return doActions(actionsFor(tui.DoubleClick))
}
}
@@ -4093,7 +4230,7 @@ func (t *Terminal) Loop() error {
// Prompt
t.cx = mxCons + t.xoffset
} else if my >= min {
- t.vset(t.offset + my - min)
+ t.vset(cy)
req(reqList)
evt := tui.RightClick
if me.Mod {
@@ -4312,26 +4449,66 @@ func (t *Terminal) Loop() error {
func (t *Terminal) constrain() {
// count of items to display allowed by filtering
count := t.merger.Length()
- // count of lines can be displayed
- height := t.maxItems()
+ maxItems := t.maxItems()
+
+ // May need to try again after adjusting the offset
+ for tries := 0; tries < maxItems; tries++ {
+ height := maxItems
+ // How many items can be fit on screen including the current item?
+ if t.multiLine && t.merger.Length() > 0 {
+ actualHeight := 0
+ linesSum := 0
+
+ add := func(i int) bool {
+ lines := t.merger.Get(i).item.text.NumLines(height - linesSum)
+ linesSum += lines
+ if linesSum >= height {
+ if actualHeight == 0 {
+ actualHeight = 1
+ }
+ return false
+ }
+ actualHeight++
+ return true
+ }
- t.cy = util.Constrain(t.cy, 0, util.Max(0, count-1))
+ for i := t.offset; i < t.merger.Length(); i++ {
+ if !add(i) {
+ break
+ }
+ }
- minOffset := util.Max(t.cy-height+1, 0)
- maxOffset := util.Max(util.Min(count-height, t.cy), 0)
- t.offset = util.Constrain(t.offset, minOffset, maxOffset)
- if t.scrollOff == 0 {
- return
- }
+ // We can possibly fit more items "before" the offset on screen
+ if linesSum < height {
+ for i := t.offset - 1; i >= 0; i-- {
+ if !add(i) {
+ break
+ }
+ }
+ }
- scrollOff := util.Min(height/2, t.scrollOff)
- for {
- prevOffset := t.offset
- if t.cy-t.offset < scrollOff {
- t.offset = util.Max(minOffset, t.offset-1)
+ height = actualHeight
}
- if t.cy-t.offset >= height-scrollOff {
- t.offset = util.Min(maxOffset, t.offset+1)
+
+ t.cy = util.Constrain(t.cy, 0, util.Max(0, count-1))
+ minOffset := util.Max(t.cy-height+1, 0)
+ maxOffset := util.Max(util.Min(count-height, t.cy), 0)
+ prevOffset := t.offset
+ t.offset = util.Constrain(t.offset, minOffset, maxOffset)
+ if t.scrollOff > 0 {
+ scrollOff := util.Min(height/2, t.scrollOff)
+ for {
+ prevOffset := t.offset
+ if t.cy-t.offset < scrollOff {
+ t.offset = util.Max(minOffset, t.offset-1)
+ }
+ if t.cy-t.offset >= height-scrollOff {
+ t.offset = util.Min(maxOffset, t.offset+1)
+ }
+ if t.offset == prevOffset {
+ break
+ }
+ }
}
if t.offset == prevOffset {
break