You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
fzf/src/terminal.go

1049 lines
21 KiB
Go

package fzf
import (
"bytes"
"fmt"
"os"
"os/exec"
"os/signal"
"regexp"
"sort"
"strings"
"sync"
"syscall"
"time"
C "github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util"
"github.com/junegunn/go-runewidth"
)
// Terminal represents terminal input/output
type Terminal struct {
inlineInfo bool
prompt string
reverse bool
hscroll bool
cx int
cy int
offset int
yanked []rune
input []rune
multi bool
sort bool
toggleSort bool
expect map[int]string
keymap map[int]actionType
execmap map[int]string
pressed string
printQuery bool
history *History
cycle bool
header []string
count int
progress int
reading bool
merger *Merger
selected map[uint32]selectedItem
reqBox *util.EventBox
eventBox *util.EventBox
mutex sync.Mutex
initFunc func()
suppress bool
startChan chan bool
}
type selectedItem struct {
at time.Time
text *string
}
type byTimeOrder []selectedItem
func (a byTimeOrder) Len() int {
return len(a)
}
func (a byTimeOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a byTimeOrder) Less(i, j int) bool {
return a[i].at.Before(a[j].at)
}
var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
var _runeWidths = make(map[rune]int)
const (
reqPrompt util.EventType = iota
reqInfo
reqHeader
reqList
reqRefresh
reqRedraw
reqClose
reqQuit
)
type actionType int
const (
actIgnore actionType = iota
actInvalid
actRune
actMouse
actBeginningOfLine
actAbort
actAccept
actBackwardChar
actBackwardDeleteChar
actBackwardWord
actCancel
actClearScreen
actDeleteChar
actDeleteCharEof
actEndOfLine
actForwardChar
actForwardWord
actKillLine
actKillWord
actUnixLineDiscard
actUnixWordRubout
actYank
actBackwardKillWord
actSelectAll
actDeselectAll
actToggle
actToggleAll
actToggleDown
actToggleUp
actDown
actUp
actPageUp
actPageDown
actToggleSort
actPreviousHistory
actNextHistory
actExecute
)
func defaultKeymap() map[int]actionType {
keymap := make(map[int]actionType)
keymap[C.Invalid] = actInvalid
keymap[C.CtrlA] = actBeginningOfLine
keymap[C.CtrlB] = actBackwardChar
keymap[C.CtrlC] = actAbort
keymap[C.CtrlG] = actAbort
keymap[C.CtrlQ] = actAbort
keymap[C.ESC] = actAbort
keymap[C.CtrlD] = actDeleteCharEof
keymap[C.CtrlE] = actEndOfLine
keymap[C.CtrlF] = actForwardChar
keymap[C.CtrlH] = actBackwardDeleteChar
keymap[C.BSpace] = actBackwardDeleteChar
keymap[C.Tab] = actToggleDown
keymap[C.BTab] = actToggleUp
keymap[C.CtrlJ] = actDown
keymap[C.CtrlK] = actUp
keymap[C.CtrlL] = actClearScreen
keymap[C.CtrlM] = actAccept
keymap[C.CtrlN] = actDown
keymap[C.CtrlP] = actUp
keymap[C.CtrlU] = actUnixLineDiscard
keymap[C.CtrlW] = actUnixWordRubout
keymap[C.CtrlY] = actYank
keymap[C.AltB] = actBackwardWord
keymap[C.SLeft] = actBackwardWord
keymap[C.AltF] = actForwardWord
keymap[C.SRight] = actForwardWord
keymap[C.AltD] = actKillWord
keymap[C.AltBS] = actBackwardKillWord
keymap[C.Up] = actUp
keymap[C.Down] = actDown
keymap[C.Left] = actBackwardChar
keymap[C.Right] = actForwardChar
keymap[C.Home] = actBeginningOfLine
keymap[C.End] = actEndOfLine
keymap[C.Del] = actDeleteChar
keymap[C.PgUp] = actPageUp
keymap[C.PgDn] = actPageDown
keymap[C.Rune] = actRune
keymap[C.Mouse] = actMouse
return keymap
}
// NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := []rune(opts.Query)
return &Terminal{
inlineInfo: opts.InlineInfo,
prompt: opts.Prompt,
reverse: opts.Reverse,
hscroll: opts.Hscroll,
cx: len(input),
cy: 0,
offset: 0,
yanked: []rune{},
input: input,
multi: opts.Multi,
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
expect: opts.Expect,
keymap: opts.Keymap,
execmap: opts.Execmap,
pressed: "",
printQuery: opts.PrintQuery,
history: opts.History,
cycle: opts.Cycle,
header: opts.Header,
reading: true,
merger: EmptyMerger,
selected: make(map[uint32]selectedItem),
reqBox: util.NewEventBox(),
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
startChan: make(chan bool, 1),
initFunc: func() {
C.Init(opts.Theme, opts.Black, opts.Mouse)
}}
}
// Input returns current query string
func (t *Terminal) Input() []rune {
t.mutex.Lock()
defer t.mutex.Unlock()
return copySlice(t.input)
}
// UpdateCount updates the count information
func (t *Terminal) UpdateCount(cnt int, final bool) {
t.mutex.Lock()
t.count = cnt
t.reading = !final
t.mutex.Unlock()
t.reqBox.Set(reqInfo, nil)
if final {
t.reqBox.Set(reqRefresh, nil)
}
}
// UpdateHeader updates the header
func (t *Terminal) UpdateHeader(header []string, lines int) {
t.mutex.Lock()
t.header = make([]string, lines)
copy(t.header, header)
if !t.reverse {
reversed := make([]string, lines)
for idx, str := range t.header {
reversed[lines-idx-1] = str
}
t.header = reversed
}
t.mutex.Unlock()
t.reqBox.Set(reqHeader, nil)
}
// UpdateProgress updates the search progress
func (t *Terminal) UpdateProgress(progress float32) {
t.mutex.Lock()
newProgress := int(progress * 100)
changed := t.progress != newProgress
t.progress = newProgress
t.mutex.Unlock()
if changed {
t.reqBox.Set(reqInfo, nil)
}
}
// UpdateList updates Merger to display the list
func (t *Terminal) UpdateList(merger *Merger) {
t.mutex.Lock()
t.progress = 100
t.merger = merger
t.mutex.Unlock()
t.reqBox.Set(reqInfo, nil)
t.reqBox.Set(reqList, nil)
}
func (t *Terminal) output() {
if t.printQuery {
fmt.Println(string(t.input))
}
if len(t.expect) > 0 {
fmt.Println(t.pressed)
}
if len(t.selected) == 0 {
cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy {
fmt.Println(t.merger.Get(t.cy).AsString())
}
} else {
sels := make([]selectedItem, 0, len(t.selected))
for _, sel := range t.selected {
sels = append(sels, sel)
}
sort.Sort(byTimeOrder(sels))
for _, sel := range sels {
fmt.Println(*sel.text)
}
}
}
func runeWidth(r rune, prefixWidth int) int {
if r == '\t' {
return 8 - prefixWidth%8
} else if w, found := _runeWidths[r]; found {
return w
} else {
w := runewidth.RuneWidth(r)
_runeWidths[r] = w
return w
}
}
func displayWidth(runes []rune) int {
l := 0
for _, r := range runes {
l += runeWidth(r, l)
}
return l
}
func (t *Terminal) move(y int, x int, clear bool) {
maxy := C.MaxY()
if !t.reverse {
y = maxy - y - 1
}
if clear {
C.MoveAndClear(y, x)
} else {
C.Move(y, x)
}
}
func (t *Terminal) placeCursor() {
t.move(0, len(t.prompt)+displayWidth(t.input[:t.cx]), false)
}
func (t *Terminal) printPrompt() {
t.move(0, 0, true)
C.CPrint(C.ColPrompt, true, t.prompt)
C.CPrint(C.ColNormal, true, string(t.input))
}
func (t *Terminal) printInfo() {
if t.inlineInfo {
t.move(0, len(t.prompt)+displayWidth(t.input)+1, true)
if t.reading {
C.CPrint(C.ColSpinner, true, " < ")
} else {
C.CPrint(C.ColPrompt, true, " < ")
}
} else {
t.move(1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
C.CPrint(C.ColSpinner, true, _spinner[idx])
}
t.move(1, 2, false)
}
output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count)
if t.toggleSort {
if t.sort {
output += "/S"
} else {
output += " "
}
}
if t.multi && len(t.selected) > 0 {
output += fmt.Sprintf(" (%d)", len(t.selected))
}
if t.progress > 0 && t.progress < 100 {
output += fmt.Sprintf(" (%d%%)", t.progress)
}
C.CPrint(C.ColInfo, false, output)
}
func (t *Terminal) printHeader() {
if len(t.header) == 0 {
return
}
max := C.MaxY()
var state *ansiState
for idx, lineStr := range t.header {
if !t.reverse {
idx = len(t.header) - idx - 1
}
line := idx + 2
if t.inlineInfo {
line -= 1
}
if line >= max {
continue
}
trimmed, colors, newState := extractColor(&lineStr, state)
state = newState
item := &Item{
text: trimmed,
index: 0,
colors: colors,
rank: Rank{0, 0, 0}}
t.move(line, 2, true)
t.printHighlighted(item, false, C.ColHeader, 0, false)
}
}
func (t *Terminal) printList() {
t.constrain()
maxy := t.maxItems()
count := t.merger.Length() - t.offset
for i := 0; i < maxy; i++ {
line := i + 2 + len(t.header)
if t.inlineInfo {
line -= 1
}
t.move(line, 0, true)
if i < count {
t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset)
}
}
}
func (t *Terminal) printItem(item *Item, current bool) {
_, selected := t.selected[item.index]
if current {
C.CPrint(C.ColCursor, true, ">")
if selected {
C.CPrint(C.ColSelected, true, ">")
} else {
C.CPrint(C.ColCurrent, true, " ")
}
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true)
} else {
C.CPrint(C.ColCursor, true, " ")
if selected {
C.CPrint(C.ColSelected, true, ">")
} else {
C.Print(" ")
}
t.printHighlighted(item, false, 0, C.ColMatch, false)
}
}
func trimRight(runes []rune, width int) ([]rune, int) {
// We start from the beginning to handle tab characters
l := 0
for idx, r := range runes {
l += runeWidth(r, l)
if idx > 0 && l > width {
return runes[:idx], len(runes) - idx
}
}
return runes, 0
}
func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
l := 0
for _, r := range runes {
l += runeWidth(r, l+prefixWidth)
if l > limit {
// Early exit
return l
}
}
return l
}
func trimLeft(runes []rune, width int) ([]rune, int32) {
currentWidth := displayWidth(runes)
var trimmed int32
for currentWidth > width && len(runes) > 0 {
runes = runes[1:]
trimmed++
currentWidth = displayWidthWithLimit(runes, 2, width)
}
return runes, trimmed
}
func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) {
var maxe int32
for _, offset := range item.offsets {
if offset[1] > maxe {
maxe = offset[1]
}
}
// Overflow
text := []rune(*item.text)
offsets := item.colorOffsets(col2, bold, current)
maxWidth := C.MaxX() - 3
fullWidth := displayWidth(text)
if fullWidth > maxWidth {
if t.hscroll {
// Stri..
matchEndWidth := displayWidth(text[:maxe])
if matchEndWidth <= maxWidth-2 {
text, _ = trimRight(text, maxWidth-2)
text = append(text, []rune("..")...)
} else {
// Stri..
if matchEndWidth < fullWidth-2 {
text = append(text[:maxe], []rune("..")...)
}
// ..ri..
var diff int32
text, diff = trimLeft(text, maxWidth-2)
// Transform offsets
for idx, offset := range offsets {
b, e := offset.offset[0], offset.offset[1]
b += 2 - diff
e += 2 - diff
b = util.Max32(b, 2)
offsets[idx].offset[0] = b
offsets[idx].offset[1] = util.Max32(b, e)
}
text = append([]rune(".."), text...)
}
} else {
text, _ = trimRight(text, maxWidth-2)
text = append(text, []rune("..")...)
for idx, offset := range offsets {
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-2))
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
}
}
}
var index int32
var substr string
var prefixWidth int
maxOffset := int32(len(text))
for _, offset := range offsets {
b := util.Constrain32(offset.offset[0], index, maxOffset)
e := util.Constrain32(offset.offset[1], index, maxOffset)
substr, prefixWidth = processTabs(text[index:b], prefixWidth)
C.CPrint(col1, bold, substr)
if b < e {
substr, prefixWidth = processTabs(text[b:e], prefixWidth)
C.CPrint(offset.color, offset.bold, substr)
}
index = e
if index >= maxOffset {
break
}
}
if index < maxOffset {
substr, _ = processTabs(text[index:], prefixWidth)
C.CPrint(col1, bold, substr)
}
}
func processTabs(runes []rune, prefixWidth int) (string, int) {
var strbuf bytes.Buffer
l := prefixWidth
for _, r := range runes {
w := runeWidth(r, l)
l += w
if r == '\t' {
strbuf.WriteString(strings.Repeat(" ", w))
} else {
strbuf.WriteRune(r)
}
}
return strbuf.String(), l
}
func (t *Terminal) printAll() {
t.printList()
t.printPrompt()
t.printInfo()
t.printHeader()
}
func (t *Terminal) refresh() {
if !t.suppress {
C.Refresh()
}
}
func (t *Terminal) delChar() bool {
if len(t.input) > 0 && t.cx < len(t.input) {
t.input = append(t.input[:t.cx], t.input[t.cx+1:]...)
return true
}
return false
}
func findLastMatch(pattern string, str string) int {
rx, err := regexp.Compile(pattern)
if err != nil {
return -1
}
locs := rx.FindAllStringIndex(str, -1)
if locs == nil {
return -1
}
return locs[len(locs)-1][0]
}
func findFirstMatch(pattern string, str string) int {
rx, err := regexp.Compile(pattern)
if err != nil {
return -1
}
loc := rx.FindStringIndex(str)
if loc == nil {
return -1
}
return loc[0]
}
func copySlice(slice []rune) []rune {
ret := make([]rune, len(slice))
copy(ret, slice)
return ret
}
func (t *Terminal) rubout(pattern string) {
pcx := t.cx
after := t.input[t.cx:]
t.cx = findLastMatch(pattern, string(t.input[:t.cx])) + 1
t.yanked = copySlice(t.input[t.cx:pcx])
t.input = append(t.input[:t.cx], after...)
}
func keyMatch(key int, event C.Event) bool {
return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ
}
func executeCommand(template string, current string) {
command := strings.Replace(template, "{}", fmt.Sprintf("%q", current), -1)
cmd := exec.Command("sh", "-c", command)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
C.Endwin()
cmd.Run()
C.Refresh()
}
// Loop is called to start Terminal I/O
func (t *Terminal) Loop() {
<-t.startChan
{ // Late initialization
t.mutex.Lock()
t.initFunc()
t.printPrompt()
t.placeCursor()
C.Refresh()
t.printInfo()
t.printHeader()
t.mutex.Unlock()
go func() {
timer := time.NewTimer(initialDelay)
<-timer.C
t.reqBox.Set(reqRefresh, nil)
}()
resizeChan := make(chan os.Signal, 1)
signal.Notify(resizeChan, syscall.SIGWINCH)
go func() {
for {
<-resizeChan
t.reqBox.Set(reqRedraw, nil)
}
}()
// Keep the spinner spinning
go func() {
for {
t.mutex.Lock()
reading := t.reading
t.mutex.Unlock()
if !reading {
break
}
time.Sleep(spinnerDuration)
t.reqBox.Set(reqInfo, nil)
}
}()
}
exit := func(code int) {
if code == 0 && t.history != nil {
t.history.append(string(t.input))
}
os.Exit(code)
}
go func() {
for {
t.reqBox.Wait(func(events *util.Events) {
defer events.Clear()
t.mutex.Lock()
for req := range *events {
switch req {
case reqPrompt:
t.printPrompt()
if t.inlineInfo {
t.printInfo()
}
case reqInfo:
t.printInfo()
case reqList:
t.printList()
case reqHeader:
t.printHeader()
case reqRefresh:
t.suppress = false
case reqRedraw:
C.Clear()
C.Endwin()
C.Refresh()
t.printAll()
case reqClose:
C.Close()
t.output()
exit(0)
case reqQuit:
C.Close()
exit(1)
}
}
t.placeCursor()
t.mutex.Unlock()
})
t.refresh()
}
}()
looping := true
for looping {
event := C.GetChar()
t.mutex.Lock()
previousInput := t.input
events := []util.EventType{reqPrompt}
req := func(evts ...util.EventType) {
for _, event := range evts {
events = append(events, event)
if event == reqClose || event == reqQuit {
looping = false
}
}
}
selectItem := func(item *Item) bool {
if _, found := t.selected[item.index]; !found {
t.selected[item.index] = selectedItem{time.Now(), item.StringPtr()}
return true
}
return false
}
toggleY := func(y int) {
item := t.merger.Get(y)
if !selectItem(item) {
delete(t.selected, item.index)
}
}
toggle := func() {
if t.cy < t.merger.Length() {
toggleY(t.cy)
req(reqInfo)
}
}
for key, ret := range t.expect {
if keyMatch(key, event) {
t.pressed = ret
req(reqClose)
break
}
}
action := t.keymap[event.Type]
mapkey := event.Type
if event.Type == C.Rune {
mapkey = int(event.Char) + int(C.AltZ)
if act, prs := t.keymap[mapkey]; prs {
action = act
}
}
switch action {
case actIgnore:
case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy)
executeCommand(t.execmap[mapkey], item.AsString())
}
case actInvalid:
t.mutex.Unlock()
continue
case actToggleSort:
t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock()
continue
case actBeginningOfLine:
t.cx = 0
case actBackwardChar:
if t.cx > 0 {
t.cx--
}
case actAbort:
req(reqQuit)
case actDeleteChar:
t.delChar()
case actDeleteCharEof:
if !t.delChar() && t.cx == 0 {
req(reqQuit)
}
case actEndOfLine:
t.cx = len(t.input)
case actCancel:
if len(t.input) == 0 {
req(reqQuit)
} else {
t.yanked = t.input
t.input = []rune{}
t.cx = 0
}
case actForwardChar:
if t.cx < len(t.input) {
t.cx++
}
case actBackwardDeleteChar:
if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
t.cx--
}
case actSelectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
selectItem(item)
}
req(reqList, reqInfo)
}
case actDeselectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
delete(t.selected, item.index)
}
req(reqList, reqInfo)
}
case actToggle:
if t.multi && t.merger.Length() > 0 {
toggle()
req(reqList)
}
case actToggleAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
toggleY(i)
}
req(reqList, reqInfo)
}
case actToggleDown:
if t.multi && t.merger.Length() > 0 {
toggle()
t.vmove(-1)
req(reqList)
}
case actToggleUp:
if t.multi && t.merger.Length() > 0 {
toggle()
t.vmove(1)
req(reqList)
}
case actDown:
t.vmove(-1)
req(reqList)
case actUp:
t.vmove(1)
req(reqList)
case actAccept:
req(reqClose)
case actClearScreen:
req(reqRedraw)
case actUnixLineDiscard:
if t.cx > 0 {
t.yanked = copySlice(t.input[:t.cx])
t.input = t.input[t.cx:]
t.cx = 0
}
case actUnixWordRubout:
if t.cx > 0 {
t.rubout("\\s\\S")
}
case actBackwardKillWord:
if t.cx > 0 {
t.rubout("[^[:alnum:]][[:alnum:]]")
}
case actYank:
suffix := copySlice(t.input[t.cx:])
t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
t.cx += len(t.yanked)
case actPageUp:
t.vmove(t.maxItems() - 1)
req(reqList)
case actPageDown:
t.vmove(-(t.maxItems() - 1))
req(reqList)
case actBackwardWord:
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
case actForwardWord:
t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
case actKillWord:
ncx := t.cx +
findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
if ncx > t.cx {
t.yanked = copySlice(t.input[t.cx:ncx])
t.input = append(t.input[:t.cx], t.input[ncx:]...)
}
case actKillLine:
if t.cx < len(t.input) {
t.yanked = copySlice(t.input[t.cx:])
t.input = t.input[:t.cx]
}
case actRune:
prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
t.cx++
case actPreviousHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = []rune(t.history.previous())
t.cx = len(t.input)
}
case actNextHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = []rune(t.history.next())
t.cx = len(t.input)
}
case actMouse:
me := event.MouseEvent
mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y
if !t.reverse {
my = C.MaxY() - my - 1
}
min := 2 + len(t.header)
if t.inlineInfo {
min -= 1
}
if me.S != 0 {
// Scroll
if t.merger.Length() > 0 {
if t.multi && me.Mod {
toggle()
}
t.vmove(me.S)
req(reqList)
}
} else if me.Double {
// Double-click
if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
req(reqClose)
}
}
} else if me.Down {
if my == 0 && mx >= 0 {
// Prompt
t.cx = mx
} else if my >= min {
// List
if t.vset(t.offset+my-min) && t.multi && me.Mod {
toggle()
}
req(reqList)
}
}
}
changed := string(previousInput) != string(t.input)
t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed {
t.eventBox.Set(EvtSearchNew, t.sort)
}
for _, event := range events {
t.reqBox.Set(event, nil)
}
}
}
func (t *Terminal) constrain() {
count := t.merger.Length()
height := t.maxItems()
diffpos := t.cy - t.offset
t.cy = util.Constrain(t.cy, 0, count-1)
if t.cy > t.offset+(height-1) {
// Ceil
t.offset = t.cy - (height - 1)
} else if t.offset > t.cy {
// Floor
t.offset = t.cy
}
// Adjustment
if count-t.offset < height {
t.offset = util.Max(0, count-height)
t.cy = util.Constrain(t.offset+diffpos, 0, count-1)
}
t.offset = util.Max(0, t.offset)
}
func (t *Terminal) vmove(o int) {
if t.reverse {
o *= -1
}
dest := t.cy + o
if t.cycle {
max := t.merger.Length() - 1
if dest > max {
if t.cy == max {
dest = 0
}
} else if dest < 0 {
if t.cy == 0 {
dest = max
}
}
}
t.vset(dest)
}
func (t *Terminal) vset(o int) bool {
t.cy = util.Constrain(o, 0, t.merger.Length()-1)
return t.cy == o
}
func (t *Terminal) maxItems() int {
max := C.MaxY() - 2 - len(t.header)
if t.inlineInfo {
max += 1
}
return util.Max(max, 0)
}