fzf/src/terminal.go

2891 lines
72 KiB
Go
Raw Normal View History

2015-01-01 19:49:30 +00:00
package fzf
import (
"bufio"
2015-01-01 19:49:30 +00:00
"fmt"
"io/ioutil"
"math"
2015-01-01 19:49:30 +00:00
"os"
2015-01-23 11:30:50 +00:00
"os/signal"
2015-01-01 19:49:30 +00:00
"regexp"
"sort"
"strconv"
2015-01-18 07:59:04 +00:00
"strings"
2015-01-01 19:49:30 +00:00
"sync"
2015-01-23 11:30:50 +00:00
"syscall"
2015-01-01 19:49:30 +00:00
"time"
2015-01-12 03:56:17 +00:00
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
"github.com/junegunn/fzf/src/tui"
2015-01-12 03:56:17 +00:00
"github.com/junegunn/fzf/src/util"
2015-01-01 19:49:30 +00:00
)
2016-09-07 00:58:18 +00:00
// import "github.com/pkg/profile"
/*
Placeholder regex is used to extract placeholders from fzf's template
strings. Acts as input validation for parsePlaceholder function.
Describes the syntax, but it is fairly lenient.
The following pseudo regex has been reverse engineered from the
implementation. It is overly strict, but better describes whats possible.
As such it is not useful for validation, but rather to generate test
cases for example.
\\?(?: # escaped type
{\+?s?f?RANGE(?:,RANGE)*} # token type
|{q} # query type
|{\+?n?f?} # item type (notice no mandatory element inside brackets)
)
RANGE = (?:
(?:-?[0-9]+)?\.\.(?:-?[0-9]+)? # ellipsis syntax for token range (x..y)
|-?[0-9]+ # shorthand syntax (x..x)
)
*/
var placeholder *regexp.Regexp
var whiteSuffix *regexp.Regexp
var offsetComponentRegex *regexp.Regexp
var offsetTrimCharsRegex *regexp.Regexp
var activeTempFiles []string
const ellipsis string = ".."
const clearCode string = "\x1b[2J"
func init() {
Improvements to code quality and readability (#1737) * Remove 1 unused field and 3 unused functions unused elements fount by running golangci-lint run --disable-all --enable unused src/result.go:19:2: field `index` is unused (unused) index int32 ^ src/tui/light.go:716:23: func `(*LightWindow).stderr` is unused (unused) func (w *LightWindow) stderr(str string) { ^ src/terminal.go:1015:6: func `numLinesMax` is unused (unused) func numLinesMax(str string, max int) int { ^ src/tui/tui.go:167:20: func `ColorPair.is24` is unused (unused) func (p ColorPair) is24() bool { ^ * Address warnings from "gosimple" linter src/options.go:389:83: S1003: should use strings.Contains(str, ",,,") instead (gosimple) if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Index(str, ",,,") >= 0 { ^ src/options.go:630:18: S1007: should use raw string (`...`) with regexp.MustCompile to avoid having to escape twice (gosimple) executeRegexp = regexp.MustCompile( ^ src/terminal.go:29:16: S1007: should use raw string (`...`) with regexp.MustCompile to avoid having to escape twice (gosimple) placeholder = regexp.MustCompile("\\\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\\+?f?nf?})") ^ src/terminal_test.go:92:10: S1007: should use raw string (`...`) with regexp.MustCompile to avoid having to escape twice (gosimple) regex = regexp.MustCompile("\\w+") ^ * Address warnings from "staticcheck" linter src/algo/algo.go:374:2: SA4006: this value of `offset32` is never used (staticcheck) offset32, T := alloc32(offset32, slab, N) ^ src/algo/algo.go:456:2: SA4006: this value of `offset16` is never used (staticcheck) offset16, C := alloc16(offset16, slab, width*M) ^ src/tui/tui.go:119:2: SA9004: only the first constant in this group has an explicit type (staticcheck) colUndefined Color = -2 ^
2019-11-05 00:46:51 +00:00
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`)
whiteSuffix = regexp.MustCompile(`\s*$`)
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
activeTempFiles = []string{}
}
type jumpMode int
const (
jumpDisabled jumpMode = iota
jumpEnabled
jumpAcceptEnabled
)
type previewer struct {
2020-10-24 07:55:55 +00:00
version int64
lines []string
offset int
enabled bool
scrollable bool
final bool
2020-12-05 12:16:35 +00:00
following bool
spinner string
}
type previewed struct {
2020-10-24 07:55:55 +00:00
version int64
numLines int
offset int
filled bool
}
type eachLine struct {
line string
err error
}
2017-01-07 16:30:31 +00:00
type itemLine struct {
2017-01-07 17:29:31 +00:00
current bool
selected bool
label string
queryLen int
width int
2017-01-07 17:29:31 +00:00
result Result
2017-01-07 16:30:31 +00:00
}
var emptyLine = itemLine{}
2015-01-11 18:01:24 +00:00
// Terminal represents terminal input/output
2015-01-01 19:49:30 +00:00
type Terminal struct {
initDelay time.Duration
infoStyle infoStyle
spinner []string
prompt func()
promptLen int
pointer string
pointerLen int
pointerEmpty string
marker string
markerLen int
markerEmpty string
queryLen [2]int
layout layoutType
fullscreen bool
keepRight bool
hscroll bool
hscrollOff int
scrollOff int
wordRubout string
wordNext string
cx int
cy int
offset int
xoffset int
yanked []rune
input []rune
multi int
sort bool
toggleSort bool
delimiter Delimiter
expect map[tui.Event]string
keymap map[tui.Event][]*action
pressed string
printQuery bool
history *History
cycle bool
headerFirst bool
headerLines int
header []string
header0 []string
ansi bool
tabstop int
margin [4]sizeSpec
padding [4]sizeSpec
strong tui.Attr
unicode bool
borderShape tui.BorderShape
cleanExit bool
paused bool
border tui.Window
window tui.Window
pborder tui.Window
pwindow tui.Window
count int
progress int
reading bool
running bool
failed *string
jumping jumpMode
jumpLabels string
printer func(string)
printsep string
merger *Merger
selected map[int32]selectedItem
version int64
reqBox *util.EventBox
initialPreviewOpts previewOpts
previewOpts previewOpts
previewer previewer
previewed previewed
previewBox *util.EventBox
eventBox *util.EventBox
mutex sync.Mutex
initFunc func()
prevLines []itemLine
suppress bool
sigstop bool
startChan chan bool
killChan chan int
slab *util.Slab
theme *tui.ColorTheme
tui tui.Renderer
executing *util.AtomicBool
2015-01-01 19:49:30 +00:00
}
type selectedItem struct {
at time.Time
item *Item
}
2015-03-22 07:05:54 +00:00
type byTimeOrder []selectedItem
2015-03-22 07:05:54 +00:00
func (a byTimeOrder) Len() int {
return len(a)
}
2015-03-22 07:05:54 +00:00
func (a byTimeOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
2015-03-22 07:05:54 +00:00
func (a byTimeOrder) Less(i, j int) bool {
return a[i].at.Before(a[j].at)
}
2015-01-01 19:49:30 +00:00
const (
2015-01-12 03:56:17 +00:00
reqPrompt util.EventType = iota
2015-01-11 18:01:24 +00:00
reqInfo
2015-07-21 18:21:20 +00:00
reqHeader
2015-01-11 18:01:24 +00:00
reqList
reqJump
2015-01-11 18:01:24 +00:00
reqRefresh
2017-04-28 13:58:08 +00:00
reqReinit
2015-01-11 18:01:24 +00:00
reqRedraw
reqFullRedraw
2015-01-11 18:01:24 +00:00
reqClose
reqPrintQuery
reqPreviewEnqueue
reqPreviewDisplay
reqPreviewRefresh
reqPreviewDelayed
2015-01-11 18:01:24 +00:00
reqQuit
2015-01-01 19:49:30 +00:00
)
type action struct {
t actionType
a string
}
2015-05-20 12:25:15 +00:00
type actionType int
const (
actIgnore actionType = iota
actInvalid
actRune
actMouse
actBeginningOfLine
actAbort
actAccept
actAcceptNonEmpty
2015-05-20 12:25:15 +00:00
actBackwardChar
actBackwardDeleteChar
actBackwardDeleteCharEOF
2015-05-20 12:25:15 +00:00
actBackwardWord
2015-07-23 12:05:33 +00:00
actCancel
2020-12-04 11:34:41 +00:00
actChangePrompt
2015-05-20 12:25:15 +00:00
actClearScreen
actClearQuery
actClearSelection
2021-02-01 15:08:54 +00:00
actClose
2015-05-20 12:25:15 +00:00
actDeleteChar
2015-08-02 04:06:15 +00:00
actDeleteCharEOF
2015-05-20 12:25:15 +00:00
actEndOfLine
actForwardChar
actForwardWord
actKillLine
actKillWord
actUnixLineDiscard
actUnixWordRubout
actYank
actBackwardKillWord
actSelectAll
actDeselectAll
actToggle
actToggleSearch
actToggleAll
2015-05-20 12:25:15 +00:00
actToggleDown
actToggleUp
actToggleIn
actToggleOut
2015-05-20 12:25:15 +00:00
actDown
actUp
actPageUp
actPageDown
2017-01-16 02:58:13 +00:00
actHalfPageUp
actHalfPageDown
actJump
actJumpAccept
actPrintQuery
2020-06-20 13:04:09 +00:00
actRefreshPreview
actReplaceQuery
2015-05-20 12:25:15 +00:00
actToggleSort
actTogglePreview
2017-02-18 14:49:00 +00:00
actTogglePreviewWrap
actPreview
actChangePreview
actChangePreviewWindow
actPreviewTop
actPreviewBottom
actPreviewUp
actPreviewDown
actPreviewPageUp
actPreviewPageDown
actPreviewHalfPageUp
actPreviewHalfPageDown
actPreviousHistory
actNextHistory
actExecute
2017-01-27 08:46:56 +00:00
actExecuteSilent
actExecuteMulti // Deprecated
2017-04-28 13:58:08 +00:00
actSigStop
actFirst
actLast
actReload
actDisableSearch
actEnableSearch
actSelect
actDeselect
2021-05-22 04:13:55 +00:00
actUnbind
2015-05-20 12:25:15 +00:00
)
type placeholderFlags struct {
plus bool
preserveSpace bool
number bool
query bool
file bool
}
type searchRequest struct {
sort bool
command *string
}
type previewRequest struct {
template string
pwindow tui.Window
scrollOffset int
list []*Item
}
type previewResult struct {
2020-10-24 07:55:55 +00:00
version int64
lines []string
offset int
spinner string
}
func toActions(types ...actionType) []*action {
actions := make([]*action, len(types))
for idx, t := range types {
actions[idx] = &action{t: t, a: ""}
}
return actions
}
func defaultKeymap() map[tui.Event][]*action {
keymap := make(map[tui.Event][]*action)
add := func(e tui.EventType, a actionType) {
keymap[e.AsEvent()] = toActions(a)
}
addEvent := func(e tui.Event, a actionType) {
keymap[e] = toActions(a)
}
add(tui.Invalid, actInvalid)
add(tui.Resize, actClearScreen)
add(tui.CtrlA, actBeginningOfLine)
add(tui.CtrlB, actBackwardChar)
add(tui.CtrlC, actAbort)
add(tui.CtrlG, actAbort)
add(tui.CtrlQ, actAbort)
add(tui.ESC, actAbort)
add(tui.CtrlD, actDeleteCharEOF)
add(tui.CtrlE, actEndOfLine)
add(tui.CtrlF, actForwardChar)
add(tui.CtrlH, actBackwardDeleteChar)
add(tui.BSpace, actBackwardDeleteChar)
add(tui.Tab, actToggleDown)
add(tui.BTab, actToggleUp)
add(tui.CtrlJ, actDown)
add(tui.CtrlK, actUp)
add(tui.CtrlL, actClearScreen)
add(tui.CtrlM, actAccept)
add(tui.CtrlN, actDown)
add(tui.CtrlP, actUp)
add(tui.CtrlU, actUnixLineDiscard)
add(tui.CtrlW, actUnixWordRubout)
add(tui.CtrlY, actYank)
2017-04-28 13:58:08 +00:00
if !util.IsWindows() {
add(tui.CtrlZ, actSigStop)
}
addEvent(tui.AltKey('b'), actBackwardWord)
add(tui.SLeft, actBackwardWord)
addEvent(tui.AltKey('f'), actForwardWord)
add(tui.SRight, actForwardWord)
addEvent(tui.AltKey('d'), actKillWord)
add(tui.AltBS, actBackwardKillWord)
add(tui.Up, actUp)
add(tui.Down, actDown)
add(tui.Left, actBackwardChar)
add(tui.Right, actForwardChar)
add(tui.Home, actBeginningOfLine)
add(tui.End, actEndOfLine)
add(tui.Del, actDeleteChar)
add(tui.PgUp, actPageUp)
add(tui.PgDn, actPageDown)
add(tui.SUp, actPreviewUp)
add(tui.SDown, actPreviewDown)
add(tui.Mouse, actMouse)
add(tui.DoubleClick, actAccept)
add(tui.LeftClick, actIgnore)
add(tui.RightClick, actToggle)
2015-05-20 12:25:15 +00:00
return keymap
}
func trimQuery(query string) []rune {
return []rune(strings.Replace(query, "\t", " ", -1))
}
func hasPreviewAction(opts *Options) bool {
for _, actions := range opts.Keymap {
for _, action := range actions {
if action.t == actPreview || action.t == actChangePreview {
return true
}
}
}
return false
}
func makeSpinner(unicode bool) []string {
if unicode {
return []string{``, ``, ``, ``, ``, ``, ``, ``, ``, ``}
}
return []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
}
2015-01-11 18:01:24 +00:00
// NewTerminal returns new Terminal object
2015-01-12 03:56:17 +00:00
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := trimQuery(opts.Query)
var header []string
switch opts.Layout {
case layoutDefault, layoutReverseList:
header = reverseStringArray(opts.Header)
default:
header = opts.Header
}
var delay time.Duration
if opts.Tac {
delay = initialDelayTac
} else {
delay = initialDelay
}
var previewBox *util.EventBox
showPreviewWindow := len(opts.Preview.command) > 0 && !opts.Preview.hidden
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) {
previewBox = util.NewEventBox()
}
2016-11-15 14:57:32 +00:00
strongAttr := tui.Bold
if !opts.Bold {
strongAttr = tui.AttrRegular
}
2017-01-07 16:30:31 +00:00
var renderer tui.Renderer
2017-04-28 13:58:08 +00:00
fullscreen := opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100
if fullscreen {
if tui.HasFullscreenRenderer() {
renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
} else {
renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
true, func(h int) int { return h })
}
} else {
2017-01-07 16:30:31 +00:00
maxHeightFunc := func(termHeight int) int {
var maxHeight int
if opts.Height.percent {
maxHeight = util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight)
2017-01-07 16:30:31 +00:00
} else {
maxHeight = int(opts.Height.size)
}
effectiveMinHeight := minHeight
if previewBox != nil && (opts.Preview.position == posUp || opts.Preview.position == posDown) {
effectiveMinHeight *= 2
2017-01-07 16:30:31 +00:00
}
if opts.InfoStyle != infoDefault {
effectiveMinHeight--
}
if opts.BorderShape != tui.BorderNone {
effectiveMinHeight += 2
2017-01-07 16:30:31 +00:00
}
return util.Min(termHeight, util.Max(maxHeight, effectiveMinHeight))
2017-01-07 16:30:31 +00:00
}
renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
2017-01-07 16:30:31 +00:00
}
wordRubout := "[^\\pL\\pN][\\pL\\pN]"
wordNext := "[\\pL\\pN][^\\pL\\pN]|(.$)"
2017-01-15 10:42:28 +00:00
if opts.FileWord {
sep := regexp.QuoteMeta(string(os.PathSeparator))
wordRubout = fmt.Sprintf("%s[^%s]", sep, sep)
wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep)
}
t := Terminal{
initDelay: delay,
infoStyle: opts.InfoStyle,
spinner: makeSpinner(opts.Unicode),
queryLen: [2]int{0, 0},
layout: opts.Layout,
fullscreen: fullscreen,
keepRight: opts.KeepRight,
hscroll: opts.Hscroll,
hscrollOff: opts.HscrollOff,
scrollOff: opts.ScrollOff,
wordRubout: wordRubout,
wordNext: wordNext,
cx: len(input),
cy: 0,
offset: 0,
xoffset: 0,
yanked: []rune{},
input: input,
multi: opts.Multi,
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
delimiter: opts.Delimiter,
expect: opts.Expect,
keymap: opts.Keymap,
pressed: "",
printQuery: opts.PrintQuery,
history: opts.History,
margin: opts.Margin,
padding: opts.Padding,
unicode: opts.Unicode,
borderShape: opts.BorderShape,
cleanExit: opts.ClearOnExit,
paused: opts.Phony,
strong: strongAttr,
cycle: opts.Cycle,
headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines,
header: header,
header0: header,
ansi: opts.Ansi,
tabstop: opts.Tabstop,
reading: true,
running: true,
failed: nil,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
printer: opts.Printer,
printsep: opts.PrintSep,
merger: EmptyMerger,
selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(),
initialPreviewOpts: opts.Preview,
previewOpts: opts.Preview,
previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""},
previewed: previewed{0, 0, 0, false},
previewBox: previewBox,
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
sigstop: false,
slab: util.MakeSlab(slab16Size, slab32Size),
theme: opts.Theme,
startChan: make(chan bool, 1),
killChan: make(chan int),
tui: renderer,
initFunc: func() { renderer.Init() },
executing: util.NewAtomicBool(false)}
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
// Pre-calculated empty pointer and marker signs
t.pointerEmpty = strings.Repeat(" ", t.pointerLen)
t.markerEmpty = strings.Repeat(" ", t.markerLen)
return &t
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) parsePrompt(prompt string) (func(), int) {
var state *ansiState
trimmed, colors, _ := extractColor(prompt, state, nil)
item := &Item{text: util.ToChars([]byte(trimmed)), colors: colors}
// "Prompt> "
// ------- // Do not apply ANSI attributes to the trailing whitespaces
// // unless the part has a non-default ANSI state
loc := whiteSuffix.FindStringIndex(trimmed)
if loc != nil {
blankState := ansiOffset{[2]int32{int32(loc[0]), int32(loc[1])}, ansiState{-1, -1, tui.AttrClear, -1}}
if item.colors != nil {
lastColor := (*item.colors)[len(*item.colors)-1]
if lastColor.offset[1] < int32(loc[1]) {
blankState.offset[0] = lastColor.offset[1]
colors := append(*item.colors, blankState)
item.colors = &colors
}
} else {
colors := []ansiOffset{blankState}
item.colors = &colors
}
}
output := func() {
t.printHighlighted(
Result{item: item}, tui.ColPrompt, tui.ColPrompt, false, false)
}
_, promptLen := t.processTabs([]rune(trimmed), 0)
return output, promptLen
}
func (t *Terminal) noInfoLine() bool {
return t.infoStyle != infoDefault
}
2015-01-11 18:01:24 +00:00
// Input returns current query string
func (t *Terminal) Input() (bool, []rune) {
2015-01-01 19:49:30 +00:00
t.mutex.Lock()
defer t.mutex.Unlock()
return t.paused, copySlice(t.input)
2015-01-01 19:49:30 +00:00
}
2015-01-11 18:01:24 +00:00
// UpdateCount updates the count information
func (t *Terminal) UpdateCount(cnt int, final bool, failedCommand *string) {
2015-01-01 19:49:30 +00:00
t.mutex.Lock()
t.count = cnt
t.reading = !final
t.failed = failedCommand
2015-01-01 19:49:30 +00:00
t.mutex.Unlock()
2015-01-11 18:01:24 +00:00
t.reqBox.Set(reqInfo, nil)
if final {
2015-01-11 18:01:24 +00:00
t.reqBox.Set(reqRefresh, nil)
}
2015-01-01 19:49:30 +00:00
}
func reverseStringArray(input []string) []string {
size := len(input)
reversed := make([]string, size)
for idx, str := range input {
reversed[size-idx-1] = str
}
return reversed
}
2015-07-21 18:21:20 +00:00
// UpdateHeader updates the header
func (t *Terminal) UpdateHeader(header []string) {
2015-07-21 18:21:20 +00:00
t.mutex.Lock()
t.header = append(append([]string{}, t.header0...), header...)
2015-07-21 18:21:20 +00:00
t.mutex.Unlock()
t.reqBox.Set(reqHeader, nil)
}
2015-01-11 18:01:24 +00:00
// UpdateProgress updates the search progress
2015-01-01 19:49:30 +00:00
func (t *Terminal) UpdateProgress(progress float32) {
t.mutex.Lock()
newProgress := int(progress * 100)
changed := t.progress != newProgress
t.progress = newProgress
2015-01-01 19:49:30 +00:00
t.mutex.Unlock()
if changed {
2015-01-11 18:01:24 +00:00
t.reqBox.Set(reqInfo, nil)
}
2015-01-01 19:49:30 +00:00
}
2015-01-11 18:01:24 +00:00
// UpdateList updates Merger to display the list
func (t *Terminal) UpdateList(merger *Merger, reset bool) {
2015-01-01 19:49:30 +00:00
t.mutex.Lock()
t.progress = 100
t.merger = merger
if reset {
t.selected = make(map[int32]selectedItem)
}
2015-01-01 19:49:30 +00:00
t.mutex.Unlock()
2015-01-11 18:01:24 +00:00
t.reqBox.Set(reqInfo, nil)
t.reqBox.Set(reqList, nil)
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) output() bool {
2015-01-01 19:49:30 +00:00
if t.printQuery {
2016-09-17 19:52:47 +00:00
t.printer(string(t.input))
2015-01-01 19:49:30 +00:00
}
if len(t.expect) > 0 {
2016-09-17 19:52:47 +00:00
t.printer(t.pressed)
}
found := len(t.selected) > 0
if !found {
current := t.currentItem()
if current != nil {
t.printer(current.AsString(t.ansi))
found = true
2015-01-01 19:49:30 +00:00
}
} else {
2015-11-08 16:42:01 +00:00
for _, sel := range t.sortSelected() {
t.printer(sel.item.AsString(t.ansi))
2015-01-01 19:49:30 +00:00
}
}
return found
2015-01-01 19:49:30 +00:00
}
2015-11-08 16:42:01 +00:00
func (t *Terminal) sortSelected() []selectedItem {
sels := make([]selectedItem, 0, len(t.selected))
for _, sel := range t.selected {
sels = append(sels, sel)
}
sort.Sort(byTimeOrder(sels))
return sels
}
2017-01-07 16:30:31 +00:00
func (t *Terminal) displayWidth(runes []rune) int {
width, _ := util.RunesWidth(runes, 0, t.tabstop, math.MaxInt32)
return width
2015-01-01 19:49:30 +00:00
}
const (
minWidth = 4
minHeight = 4
)
2015-07-26 14:02:04 +00:00
2020-10-06 10:37:33 +00:00
func calculateSize(base int, size sizeSpec, occupied int, minSize int, pad int) int {
max := base - occupied
if size.percent {
return util.Constrain(int(float64(base)*0.01*size.size), minSize, max)
}
2020-10-06 10:37:33 +00:00
return util.Constrain(int(size.size)+pad, minSize, max)
}
func (t *Terminal) resizeWindows() {
2017-01-07 16:30:31 +00:00
screenWidth := t.tui.MaxX()
screenHeight := t.tui.MaxY()
t.prevLines = make([]itemLine, screenHeight)
2020-11-09 11:34:08 +00:00
marginInt := [4]int{} // TRBL
paddingInt := [4]int{} // TRBL
sizeSpecToInt := func(index int, spec sizeSpec) int {
if spec.percent {
var max float64
2020-11-09 11:34:08 +00:00
if index%2 == 0 {
max = float64(screenHeight)
2015-07-26 14:02:04 +00:00
} else {
max = float64(screenWidth)
2015-07-26 14:02:04 +00:00
}
2020-11-09 11:34:08 +00:00
return int(max * spec.size * 0.01)
2015-07-26 14:02:04 +00:00
}
2020-11-09 11:34:08 +00:00
return int(spec.size)
}
for idx, sizeSpec := range t.padding {
paddingInt[idx] = sizeSpecToInt(idx, sizeSpec)
}
extraMargin := [4]int{} // TRBL
for idx, sizeSpec := range t.margin {
switch t.borderShape {
case tui.BorderHorizontal:
2020-11-09 11:34:08 +00:00
extraMargin[idx] += 1 - idx%2
case tui.BorderVertical:
2020-11-09 11:34:08 +00:00
extraMargin[idx] += 2 * (idx % 2)
case tui.BorderTop:
if idx == 0 {
2020-11-09 11:34:08 +00:00
extraMargin[idx]++
}
case tui.BorderRight:
if idx == 1 {
2020-11-09 11:34:08 +00:00
extraMargin[idx] += 2
}
case tui.BorderBottom:
if idx == 2 {
2020-11-09 11:34:08 +00:00
extraMargin[idx]++
}
case tui.BorderLeft:
if idx == 3 {
2020-11-09 11:34:08 +00:00
extraMargin[idx] += 2
}
case tui.BorderRounded, tui.BorderSharp:
2020-11-09 11:34:08 +00:00
extraMargin[idx] += 1 + idx%2
}
2020-11-09 11:34:08 +00:00
marginInt[idx] = sizeSpecToInt(idx, sizeSpec) + extraMargin[idx]
2015-07-26 14:02:04 +00:00
}
2020-11-09 11:34:08 +00:00
2015-07-26 14:02:04 +00:00
adjust := func(idx1 int, idx2 int, max int, min int) {
if max >= min {
2020-11-09 11:34:08 +00:00
margin := marginInt[idx1] + marginInt[idx2] + paddingInt[idx1] + paddingInt[idx2]
2015-07-26 14:02:04 +00:00
if max-margin < min {
desired := max - min
2020-11-09 11:34:08 +00:00
paddingInt[idx1] = desired * paddingInt[idx1] / margin
paddingInt[idx2] = desired * paddingInt[idx2] / margin
marginInt[idx1] = util.Max(extraMargin[idx1], desired*marginInt[idx1]/margin)
marginInt[idx2] = util.Max(extraMargin[idx2], desired*marginInt[idx2]/margin)
2015-07-26 14:02:04 +00:00
}
}
}
2020-12-05 12:16:35 +00:00
previewVisible := t.isPreviewEnabled() && t.previewOpts.size.size > 0
minAreaWidth := minWidth
minAreaHeight := minHeight
if previewVisible {
2020-12-05 12:16:35 +00:00
switch t.previewOpts.position {
case posUp, posDown:
minAreaHeight *= 2
case posLeft, posRight:
minAreaWidth *= 2
}
}
adjust(1, 3, screenWidth, minAreaWidth)
adjust(0, 2, screenHeight, minAreaHeight)
if t.border != nil {
t.border.Close()
}
if t.window != nil {
t.window.Close()
}
if t.pborder != nil {
t.pborder.Close()
}
if t.pwindow != nil {
t.pwindow.Close()
}
// Reset preview version so that full redraw occurs
t.previewed.version = 0
width := screenWidth - marginInt[1] - marginInt[3]
height := screenHeight - marginInt[0] - marginInt[2]
switch t.borderShape {
case tui.BorderHorizontal:
t.border = t.tui.NewWindow(
marginInt[0]-1, marginInt[3], width, height+2,
false, tui.MakeBorderStyle(tui.BorderHorizontal, t.unicode))
case tui.BorderVertical:
t.border = t.tui.NewWindow(
marginInt[0], marginInt[3]-2, width+4, height,
false, tui.MakeBorderStyle(tui.BorderVertical, t.unicode))
case tui.BorderTop:
t.border = t.tui.NewWindow(
marginInt[0]-1, marginInt[3], width, height+1,
false, tui.MakeBorderStyle(tui.BorderTop, t.unicode))
case tui.BorderBottom:
t.border = t.tui.NewWindow(
marginInt[0], marginInt[3], width, height+1,
false, tui.MakeBorderStyle(tui.BorderBottom, t.unicode))
case tui.BorderLeft:
t.border = t.tui.NewWindow(
marginInt[0], marginInt[3]-2, width+2, height,
false, tui.MakeBorderStyle(tui.BorderLeft, t.unicode))
case tui.BorderRight:
t.border = t.tui.NewWindow(
marginInt[0], marginInt[3], width+2, height,
false, tui.MakeBorderStyle(tui.BorderRight, t.unicode))
case tui.BorderRounded, tui.BorderSharp:
t.border = t.tui.NewWindow(
marginInt[0]-1, marginInt[3]-2, width+4, height+2,
false, tui.MakeBorderStyle(t.borderShape, t.unicode))
}
2020-11-09 11:34:08 +00:00
// Add padding
for idx, val := range paddingInt {
marginInt[idx] += val
}
width = screenWidth - marginInt[1] - marginInt[3]
height = screenHeight - marginInt[0] - marginInt[2]
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
if previewVisible {
createPreviewWindow := func(y int, x int, w int, h int) {
pwidth := w
pheight := h
var previewBorder tui.BorderStyle
if t.previewOpts.border == tui.BorderNone {
previewBorder = tui.MakeTransparentBorder()
} else {
previewBorder = tui.MakeBorderStyle(t.previewOpts.border, t.unicode)
}
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
switch t.previewOpts.border {
case tui.BorderSharp, tui.BorderRounded:
pwidth -= 4
pheight -= 2
x += 2
y += 1
case tui.BorderLeft:
pwidth -= 2
x += 2
case tui.BorderRight:
pwidth -= 2
case tui.BorderTop:
pheight -= 1
y += 1
case tui.BorderBottom:
pheight -= 1
case tui.BorderHorizontal:
pheight -= 2
y += 1
case tui.BorderVertical:
pwidth -= 4
x += 2
}
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, true, noBorder)
}
2020-10-06 10:37:33 +00:00
verticalPad := 2
minPreviewHeight := 3
2021-04-06 11:05:54 +00:00
switch t.previewOpts.border {
case tui.BorderNone, tui.BorderVertical, tui.BorderLeft, tui.BorderRight:
2020-10-06 10:37:33 +00:00
verticalPad = 0
minPreviewHeight = 1
2021-04-06 11:05:54 +00:00
case tui.BorderTop, tui.BorderBottom:
verticalPad = 1
minPreviewHeight = 2
2020-10-06 10:37:33 +00:00
}
2020-12-05 12:16:35 +00:00
switch t.previewOpts.position {
case posUp:
2020-12-05 12:16:35 +00:00
pheight := calculateSize(height, t.previewOpts.size, minHeight, minPreviewHeight, verticalPad)
2017-01-07 16:30:31 +00:00
t.window = t.tui.NewWindow(
marginInt[0]+pheight, marginInt[3], width, height-pheight, false, noBorder)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
case posDown:
2020-12-05 12:16:35 +00:00
pheight := calculateSize(height, t.previewOpts.size, minHeight, minPreviewHeight, verticalPad)
2017-01-07 16:30:31 +00:00
t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width, height-pheight, false, noBorder)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
case posLeft:
2020-12-05 12:16:35 +00:00
pwidth := calculateSize(width, t.previewOpts.size, minWidth, 5, 4)
2017-01-07 16:30:31 +00:00
t.window = t.tui.NewWindow(
marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false, noBorder)
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
case posRight:
2020-12-05 12:16:35 +00:00
pwidth := calculateSize(width, t.previewOpts.size, minWidth, 5, 4)
2017-01-07 16:30:31 +00:00
t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width-pwidth, height, false, noBorder)
createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
}
} else {
2017-01-07 16:30:31 +00:00
t.window = t.tui.NewWindow(
marginInt[0],
marginInt[3],
width,
height, false, noBorder)
}
for i := 0; i < t.window.Height(); i++ {
t.window.MoveAndClear(i, 0)
}
2015-07-26 14:02:04 +00:00
}
2015-01-01 19:49:30 +00:00
func (t *Terminal) move(y int, x int, clear bool) {
h := t.window.Height()
switch t.layout {
case layoutDefault:
y = h - y - 1
case layoutReverseList:
n := 2 + len(t.header)
if t.noInfoLine() {
n--
}
if y < n {
y = h - y - 1
} else {
y -= n
}
2015-01-01 19:49:30 +00:00
}
if clear {
t.window.MoveAndClear(y, x)
2015-01-01 19:49:30 +00:00
} else {
t.window.Move(y, x)
2015-01-01 19:49:30 +00:00
}
}
func (t *Terminal) truncateQuery() {
t.input, _ = t.trimRight(t.input, maxPatternLength)
t.cx = util.Constrain(t.cx, 0, len(t.input))
}
func (t *Terminal) updatePromptOffset() ([]rune, []rune) {
maxWidth := util.Max(1, t.window.Width()-t.promptLen-1)
_, overflow := t.trimLeft(t.input[:t.cx], maxWidth)
minOffset := int(overflow)
maxOffset := util.Min(util.Min(len(t.input), minOffset+maxWidth), t.cx)
t.xoffset = util.Constrain(t.xoffset, minOffset, maxOffset)
before, _ := t.trimLeft(t.input[t.xoffset:t.cx], maxWidth)
beforeLen := t.displayWidth(before)
after, _ := t.trimRight(t.input[t.cx:], maxWidth-beforeLen)
afterLen := t.displayWidth(after)
t.queryLen = [2]int{beforeLen, afterLen}
return before, after
}
func (t *Terminal) promptLine() int {
if t.headerFirst {
max := t.window.Height() - 1
if !t.noInfoLine() {
max--
}
return util.Min(len(t.header0)+t.headerLines, max)
}
return 0
}
2015-01-01 19:49:30 +00:00
func (t *Terminal) placeCursor() {
t.move(t.promptLine(), t.promptLen+t.queryLen[0], false)
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) printPrompt() {
t.move(t.promptLine(), 0, true)
t.prompt()
before, after := t.updatePromptOffset()
color := tui.ColInput
if t.paused {
color = tui.ColDisabled
}
t.window.CPrint(color, string(before))
t.window.CPrint(color, string(after))
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) trimMessage(message string, maxWidth int) string {
if len(message) <= maxWidth {
return message
}
runes, _ := t.trimRight([]rune(message), maxWidth-2)
return string(runes) + strings.Repeat(".", util.Constrain(maxWidth, 0, 2))
}
2015-01-01 19:49:30 +00:00
func (t *Terminal) printInfo() {
2017-02-18 14:17:29 +00:00
pos := 0
line := t.promptLine()
switch t.infoStyle {
case infoDefault:
t.move(line+1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
2020-01-15 01:43:09 +00:00
idx := (time.Now().UnixNano() % (duration * int64(len(t.spinner)))) / duration
t.window.CPrint(tui.ColSpinner, t.spinner[idx])
}
t.move(line+1, 2, false)
pos = 2
case infoInline:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
2017-02-18 14:17:29 +00:00
if pos+len(" < ") > t.window.Width() {
return
}
t.move(line, pos, true)
2015-04-21 14:50:53 +00:00
if t.reading {
t.window.CPrint(tui.ColSpinner, " < ")
2015-04-21 14:50:53 +00:00
} else {
t.window.CPrint(tui.ColPrompt, " < ")
2015-04-21 14:50:53 +00:00
}
2017-02-18 14:17:29 +00:00
pos += len(" < ")
case infoHidden:
return
2015-01-01 19:49:30 +00:00
}
found := t.merger.Length()
total := util.Max(found, t.count)
output := fmt.Sprintf("%d/%d", found, total)
2015-05-20 12:25:15 +00:00
if t.toggleSort {
if t.sort {
output += " +S"
} else {
output += " -S"
}
}
if t.multi > 0 {
if t.multi == maxMulti {
output += fmt.Sprintf(" (%d)", len(t.selected))
} else {
output += fmt.Sprintf(" (%d/%d)", len(t.selected), t.multi)
}
2015-01-01 19:49:30 +00:00
}
if t.progress > 0 && t.progress < 100 {
output += fmt.Sprintf(" (%d%%)", t.progress)
}
if t.failed != nil && t.count == 0 {
output = fmt.Sprintf("[Command failed: %s]", *t.failed)
}
output = t.trimMessage(output, t.window.Width()-pos)
t.window.CPrint(tui.ColInfo, output)
2015-07-26 14:02:04 +00:00
}
2015-07-21 15:19:37 +00:00
func (t *Terminal) printHeader() {
if len(t.header) == 0 {
return
}
2017-01-07 16:30:31 +00:00
max := t.window.Height()
if t.headerFirst {
max--
if !t.noInfoLine() {
max--
}
}
var state *ansiState
2015-07-21 15:19:37 +00:00
for idx, lineStr := range t.header {
line := idx
if !t.headerFirst {
line++
if !t.noInfoLine() {
line++
}
}
if line >= max {
continue
}
trimmed, colors, newState := extractColor(lineStr, state, nil)
state = newState
2015-07-21 15:19:37 +00:00
item := &Item{
text: util.ToChars([]byte(trimmed)),
colors: colors}
2015-07-21 15:19:37 +00:00
t.move(line, 2, true)
t.printHighlighted(Result{item: item},
tui.ColHeader, tui.ColHeader, false, false)
2015-07-21 15:19:37 +00:00
}
}
2015-01-01 19:49:30 +00:00
func (t *Terminal) printList() {
t.constrain()
2015-04-21 14:50:53 +00:00
maxy := t.maxItems()
count := t.merger.Length() - t.offset
2017-01-07 16:30:31 +00:00
for j := 0; j < maxy; j++ {
i := j
if t.layout == layoutDefault {
2017-01-07 16:30:31 +00:00
i = maxy - 1 - j
}
2015-07-21 15:19:37 +00:00
line := i + 2 + len(t.header)
if t.noInfoLine() {
2015-08-02 04:06:15 +00:00
line--
2015-04-21 14:50:53 +00:00
}
2015-01-01 19:49:30 +00:00
if i < count {
2017-01-07 16:30:31 +00:00
t.printItem(t.merger.Get(i+t.offset), line, i, i == t.cy-t.offset)
} else if t.prevLines[i] != emptyLine {
t.prevLines[i] = emptyLine
t.move(line, 0, true)
2015-01-01 19:49:30 +00:00
}
}
}
func (t *Terminal) printItem(result Result, line int, i int, current bool) {
item := result.item
_, selected := t.selected[item.Index()]
label := ""
if t.jumping != jumpDisabled {
if i < len(t.jumpLabels) {
// Striped
current = i%2 == 0
label = t.jumpLabels[i:i+1] + strings.Repeat(" ", t.pointerLen-1)
}
} else if current {
label = t.pointer
}
2017-01-07 16:30:31 +00:00
// Avoid unnecessary redraw
newLine := itemLine{current: current, selected: selected, label: label,
result: result, queryLen: len(t.input), width: 0}
prevLine := t.prevLines[i]
if prevLine.current == newLine.current &&
prevLine.selected == newLine.selected &&
prevLine.label == newLine.label &&
prevLine.queryLen == newLine.queryLen &&
prevLine.result == newLine.result {
2017-01-07 16:30:31 +00:00
return
}
t.move(line, 0, false)
2015-01-01 19:49:30 +00:00
if current {
if len(label) == 0 {
t.window.CPrint(tui.ColCurrentCursorEmpty, t.pointerEmpty)
} else {
t.window.CPrint(tui.ColCurrentCursor, label)
}
2015-01-01 19:49:30 +00:00
if selected {
t.window.CPrint(tui.ColCurrentSelected, t.marker)
2015-01-01 19:49:30 +00:00
} else {
t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty)
2015-01-01 19:49:30 +00:00
}
newLine.width = t.printHighlighted(result, tui.ColCurrent, tui.ColCurrentMatch, true, true)
2015-01-01 19:49:30 +00:00
} else {
if len(label) == 0 {
t.window.CPrint(tui.ColCursorEmpty, t.pointerEmpty)
} else {
t.window.CPrint(tui.ColCursor, label)
}
2015-01-01 19:49:30 +00:00
if selected {
t.window.CPrint(tui.ColSelected, t.marker)
2015-01-01 19:49:30 +00:00
} else {
t.window.Print(t.markerEmpty)
2015-01-01 19:49:30 +00:00
}
newLine.width = t.printHighlighted(result, tui.ColNormal, tui.ColMatch, false, true)
}
fillSpaces := prevLine.width - newLine.width
if fillSpaces > 0 {
t.window.Print(strings.Repeat(" ", fillSpaces))
2015-01-01 19:49:30 +00:00
}
t.prevLines[i] = newLine
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) trimRight(runes []rune, width int) ([]rune, bool) {
2015-01-18 07:59:04 +00:00
// We start from the beginning to handle tab characters
_, overflowIdx := util.RunesWidth(runes, 0, t.tabstop, width)
if overflowIdx >= 0 {
return runes[:overflowIdx], true
2015-01-18 07:59:04 +00:00
}
return runes, false
2015-01-18 07:59:04 +00:00
}
2015-01-01 19:49:30 +00:00
2017-01-07 16:30:31 +00:00
func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
width, _ := util.RunesWidth(runes, prefixWidth, t.tabstop, limit)
return width
2015-01-01 19:49:30 +00:00
}
2017-01-07 16:30:31 +00:00
func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) {
width = util.Max(0, width)
var trimmed int32
// Assume that each rune takes at least one column on screen
if len(runes) > width+2 {
diff := len(runes) - width - 2
trimmed = int32(diff)
runes = runes[diff:]
}
2017-01-07 16:30:31 +00:00
currentWidth := t.displayWidth(runes)
2015-01-01 19:49:30 +00:00
for currentWidth > width && len(runes) > 0 {
runes = runes[1:]
2015-01-11 18:01:24 +00:00
trimmed++
2017-01-07 16:30:31 +00:00
currentWidth = t.displayWidthWithLimit(runes, 2, width)
2015-01-01 19:49:30 +00:00
}
return runes, trimmed
}
2017-01-07 16:30:31 +00:00
func (t *Terminal) overflow(runes []rune, max int) bool {
return t.displayWidthWithLimit(runes, 0, max) > max
2016-08-14 08:44:11 +00:00
}
func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMatch tui.ColorPair, current bool, match bool) int {
item := result.item
2015-01-01 19:49:30 +00:00
// Overflow
text := make([]rune, item.text.Length())
copy(text, item.text.ToRunes())
2016-08-19 16:46:54 +00:00
matchOffsets := []Offset{}
2016-09-07 00:58:18 +00:00
var pos *[]int
if match && t.merger.pattern != nil {
2016-09-07 00:58:18 +00:00
_, matchOffsets, pos = t.merger.pattern.MatchItem(item, true, t.slab)
}
charOffsets := matchOffsets
if pos != nil {
charOffsets = make([]Offset, len(*pos))
for idx, p := range *pos {
offset := Offset{int32(p), int32(p + 1)}
charOffsets[idx] = offset
}
sort.Sort(ByOrder(charOffsets))
2016-08-19 16:46:54 +00:00
}
var maxe int
2016-09-07 00:58:18 +00:00
for _, offset := range charOffsets {
2016-08-19 16:46:54 +00:00
maxe = util.Max(maxe, int(offset[1]))
}
2016-09-07 00:58:18 +00:00
offsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
2020-09-24 02:06:20 +00:00
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
displayWidth := t.displayWidthWithLimit(text, 0, maxWidth)
if displayWidth > maxWidth {
transformOffsets := func(diff int32) {
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)
}
}
if t.hscroll {
if t.keepRight && pos == nil {
trimmed, diff := t.trimLeft(text, maxWidth-2)
transformOffsets(diff)
text = append([]rune(ellipsis), trimmed...)
} else if !t.overflow(text[:maxe], maxWidth-2) {
// Stri..
2017-01-07 16:30:31 +00:00
text, _ = t.trimRight(text, maxWidth-2)
text = append(text, []rune(ellipsis)...)
} else {
// Stri..
2017-01-07 16:30:31 +00:00
if t.overflow(text[maxe:], 2) {
text = append(text[:maxe], []rune(ellipsis)...)
}
// ..ri..
var diff int32
2017-01-07 16:30:31 +00:00
text, diff = t.trimLeft(text, maxWidth-2)
// Transform offsets
transformOffsets(diff)
text = append([]rune(ellipsis), text...)
2015-01-01 19:49:30 +00:00
}
} else {
2017-01-07 16:30:31 +00:00
text, _ = t.trimRight(text, maxWidth-2)
text = append(text, []rune(ellipsis)...)
2015-01-01 19:49:30 +00:00
2015-03-18 16:59:14 +00:00
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))
2015-01-01 19:49:30 +00:00
}
}
displayWidth = t.displayWidthWithLimit(text, 0, displayWidth)
2015-01-01 19:49:30 +00:00
}
2015-01-11 18:01:24 +00:00
var index int32
2015-01-18 07:59:04 +00:00
var substr string
var prefixWidth int
2015-03-18 16:59:14 +00:00
maxOffset := int32(len(text))
2015-01-01 19:49:30 +00:00
for _, offset := range offsets {
2015-03-18 16:59:14 +00:00
b := util.Constrain32(offset.offset[0], index, maxOffset)
e := util.Constrain32(offset.offset[1], index, maxOffset)
2015-01-18 07:59:04 +00:00
2017-01-07 16:30:31 +00:00
substr, prefixWidth = t.processTabs(text[index:b], prefixWidth)
t.window.CPrint(colBase, substr)
2015-01-18 07:59:04 +00:00
2015-03-19 03:14:26 +00:00
if b < e {
2017-01-07 16:30:31 +00:00
substr, prefixWidth = t.processTabs(text[b:e], prefixWidth)
t.window.CPrint(offset.color, substr)
2015-03-19 03:14:26 +00:00
}
2015-01-18 07:59:04 +00:00
2015-01-01 19:49:30 +00:00
index = e
2015-03-18 16:59:14 +00:00
if index >= maxOffset {
break
}
2015-01-01 19:49:30 +00:00
}
2015-03-18 16:59:14 +00:00
if index < maxOffset {
2017-01-07 16:30:31 +00:00
substr, _ = t.processTabs(text[index:], prefixWidth)
t.window.CPrint(colBase, substr)
}
return displayWidth
}
func (t *Terminal) renderPreviewSpinner() {
numLines := len(t.previewer.lines)
spin := t.previewer.spinner
if len(spin) > 0 || t.previewer.scrollable {
maxWidth := t.pwindow.Width()
if !t.previewer.scrollable {
if maxWidth > 0 {
t.pwindow.Move(0, maxWidth-1)
t.pwindow.CPrint(tui.ColSpinner, spin)
}
} else {
offsetString := fmt.Sprintf("%d/%d", t.previewer.offset+1, numLines)
if len(spin) > 0 {
spin += " "
maxWidth -= 2
}
offsetRunes, _ := t.trimRight([]rune(offsetString), maxWidth)
pos := maxWidth - t.displayWidth(offsetRunes)
t.pwindow.Move(0, pos)
if maxWidth > 0 {
t.pwindow.CPrint(tui.ColSpinner, spin)
t.pwindow.CPrint(tui.ColInfo.WithAttr(tui.Reverse), string(offsetRunes))
}
}
}
}
func (t *Terminal) renderPreviewArea(unchanged bool) {
if unchanged {
t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display
} else {
t.previewed.filled = false
t.pwindow.Erase()
}
height := t.pwindow.Height()
header := []string{}
body := t.previewer.lines
headerLines := t.previewOpts.headerLines
// Do not enable preview header lines if it's value is too large
if headerLines > 0 && headerLines < util.Min(len(body), height) {
header = t.previewer.lines[0:headerLines]
body = t.previewer.lines[headerLines:]
// Always redraw header
t.renderPreviewText(height, header, 0, false)
t.pwindow.MoveAndClear(t.pwindow.Y(), 0)
}
t.renderPreviewText(height, body, -t.previewer.offset+headerLines, unchanged)
if !unchanged {
t.pwindow.FinishFill()
}
}
func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) {
maxWidth := t.pwindow.Width()
var ansi *ansiState
for _, line := range lines {
var lbg tui.Color = -1
if ansi != nil {
ansi.lbg = -1
}
line = strings.TrimSuffix(line, "\n")
if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
t.previewed.filled = true
break
} else if lineNo >= 0 {
var fillRet tui.FillReturn
prefixWidth := 0
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
trimmed := []rune(str)
isTrimmed := false
2020-12-05 12:16:35 +00:00
if !t.previewOpts.wrap {
trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
}
str, width := t.processTabs(trimmed, prefixWidth)
prefixWidth += width
if t.theme.Colored && ansi != nil && ansi.colored() {
lbg = ansi.lbg
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
} else {
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
}
return !isTrimmed &&
(fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine)
})
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
2019-03-29 08:29:24 +00:00
if fillRet == tui.FillNextLine {
continue
2019-03-29 08:29:24 +00:00
} else if fillRet == tui.FillSuspend {
t.previewed.filled = true
break
}
if unchanged && lineNo == 0 {
break
2017-01-07 16:30:31 +00:00
}
if lbg >= 0 {
t.pwindow.CFill(-1, lbg, tui.AttrRegular,
strings.Repeat(" ", t.pwindow.Width()-t.pwindow.X())+"\n")
} else {
t.pwindow.Fill("\n")
}
}
lineNo++
}
2015-01-18 07:59:04 +00:00
}
func (t *Terminal) printPreview() {
if !t.hasPreviewWindow() {
return
}
numLines := len(t.previewer.lines)
height := t.pwindow.Height()
unchanged := (t.previewed.filled || numLines == t.previewed.numLines) &&
t.previewer.version == t.previewed.version &&
t.previewer.offset == t.previewed.offset
t.previewer.scrollable = t.previewer.offset > 0 || numLines > height
t.renderPreviewArea(unchanged)
t.renderPreviewSpinner()
t.previewed.numLines = numLines
t.previewed.version = t.previewer.version
t.previewed.offset = t.previewer.offset
}
func (t *Terminal) printPreviewDelayed() {
if !t.hasPreviewWindow() || len(t.previewer.lines) > 0 && t.previewed.version == t.previewer.version {
return
}
t.previewer.scrollable = false
t.renderPreviewArea(true)
message := t.trimMessage("Loading ..", t.pwindow.Width())
pos := t.pwindow.Width() - len(message)
t.pwindow.Move(0, pos)
t.pwindow.CPrint(tui.ColInfo.WithAttr(tui.Reverse), message)
}
2017-01-07 16:30:31 +00:00
func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
var strbuf strings.Builder
2015-01-18 07:59:04 +00:00
l := prefixWidth
gr := uniseg.NewGraphemes(string(runes))
for gr.Next() {
rs := gr.Runes()
str := string(rs)
var w int
if len(rs) == 1 && rs[0] == '\t' {
w = t.tabstop - l%t.tabstop
2015-01-18 07:59:04 +00:00
strbuf.WriteString(strings.Repeat(" ", w))
} else {
w = runewidth.StringWidth(str)
strbuf.WriteString(str)
2015-01-18 07:59:04 +00:00
}
l += w
2015-01-01 19:49:30 +00:00
}
2015-01-18 07:59:04 +00:00
return strbuf.String(), l
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) printAll() {
t.resizeWindows()
2015-01-01 19:49:30 +00:00
t.printList()
t.printPrompt()
t.printInfo()
2015-07-21 15:47:14 +00:00
t.printHeader()
t.printPreview()
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) refresh() {
2019-03-29 17:06:54 +00:00
t.placeCursor()
if !t.suppress {
windows := make([]tui.Window, 0, 4)
if t.borderShape != tui.BorderNone {
windows = append(windows, t.border)
}
if t.hasPreviewWindow() {
if t.pborder != nil {
windows = append(windows, t.pborder)
}
windows = append(windows, t.pwindow)
}
windows = append(windows, t.window)
t.tui.RefreshWindows(windows)
}
2015-01-01 19:49:30 +00:00
}
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
}
prefix := []rune(str[:locs[len(locs)-1][0]])
return len(prefix)
2015-01-01 19:49:30 +00:00
}
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
}
prefix := []rune(str[:loc[0]])
return len(prefix)
2015-01-01 19:49:30 +00:00
}
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 tui.Event, event tui.Event) bool {
return event.Type == key.Type && event.Char == key.Char ||
key.Type == tui.DoubleClick && event.Type == tui.Mouse && event.MouseEvent.Double
2015-03-31 13:05:02 +00:00
}
func parsePlaceholder(match string) (bool, string, placeholderFlags) {
flags := placeholderFlags{}
if match[0] == '\\' {
// Escaped placeholder pattern
return true, match[1:], flags
}
skipChars := 1
for _, char := range match[1:] {
switch char {
case '+':
flags.plus = true
skipChars++
case 's':
flags.preserveSpace = true
skipChars++
case 'n':
flags.number = true
skipChars++
case 'f':
flags.file = true
skipChars++
case 'q':
flags.query = true
// query flag is not skipped
default:
break
}
}
matchWithoutFlags := "{" + match[skipChars:]
return false, matchWithoutFlags, flags
}
func hasPreviewFlags(template string) (slot bool, plus bool, query bool) {
for _, match := range placeholder.FindAllString(template, -1) {
_, _, flags := parsePlaceholder(match)
if flags.plus {
plus = true
}
if flags.query {
query = true
}
slot = true
}
return
}
func writeTemporaryFile(data []string, printSep string) string {
f, err := ioutil.TempFile("", "fzf-preview-*")
if err != nil {
errorExit("Unable to create temporary file")
}
defer f.Close()
f.WriteString(strings.Join(data, printSep))
f.WriteString(printSep)
activeTempFiles = append(activeTempFiles, f.Name())
return f.Name()
}
func cleanTemporaryFiles() {
for _, filename := range activeTempFiles {
os.Remove(filename)
}
activeTempFiles = []string{}
}
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string {
return replacePlaceholder(
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
}
func (t *Terminal) evaluateScrollOffset() int {
if t.pwindow == nil {
return 0
}
// We only need the current item to calculate the scroll offset
offsetExpr := offsetTrimCharsRegex.ReplaceAllString(
t.replacePlaceholder(t.previewOpts.scroll, false, "", []*Item{t.currentItem(), nil}), "")
atoi := func(s string) int {
n, e := strconv.Atoi(s)
if e != nil {
return 0
}
return n
}
base := -1
height := util.Max(0, t.pwindow.Height()-t.previewOpts.headerLines)
for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) {
if strings.HasPrefix(component, "-/") {
component = component[1:]
}
if component[0] == '/' {
denom := atoi(component[1:])
if denom != 0 {
base -= height / denom
}
break
}
base += atoi(component)
}
return util.Max(0, base)
}
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
current := allItems[:1]
selected := allItems[1:]
if current[0] == nil {
current = []*Item{}
}
if selected[0] == nil {
selected = []*Item{}
}
// replace placeholders one by one
return placeholder.ReplaceAllStringFunc(template, func(match string) string {
escaped, match, flags := parsePlaceholder(match)
// this function implements the effects a placeholder has on items
var replace func(*Item) string
// placeholder types (escaped, query type, item type, token type)
switch {
case escaped:
return match
case match == "{q}":
return quoteEntry(query)
case match == "{}":
replace = func(item *Item) string {
switch {
case flags.number:
n := int(item.text.Index)
if n < 0 {
return ""
}
return strconv.Itoa(n)
case flags.file:
return item.AsString(stripAnsi)
default:
return quoteEntry(item.AsString(stripAnsi))
}
}
default:
// token type and also failover (below)
rangeExpressions := strings.Split(match[1:len(match)-1], ",")
ranges := make([]Range, len(rangeExpressions))
for idx, s := range rangeExpressions {
r, ok := ParseRange(&s) // ellipsis (x..y) and shorthand (x..x) range syntax
if !ok {
// Invalid expression, just return the original string in the template
return match
}
ranges[idx] = r
}
replace = func(item *Item) string {
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
trans := Transform(tokens, ranges)
str := joinTokens(trans)
// trim the last delimiter
if delimiter.str != nil {
str = strings.TrimSuffix(str, *delimiter.str)
} else if delimiter.regex != nil {
delims := delimiter.regex.FindAllStringIndex(str, -1)
// make sure the delimiter is at the very end of the string
if len(delims) > 0 && delims[len(delims)-1][1] == len(str) {
str = str[:delims[len(delims)-1][0]]
}
}
if !flags.preserveSpace {
str = strings.TrimSpace(str)
}
if !flags.file {
str = quoteEntry(str)
}
return str
}
}
// apply 'replace' function over proper set of items and return result
items := current
if flags.plus || forcePlus {
items = selected
}
replacements := make([]string, len(items))
for idx, item := range items {
replacements[idx] = replace(item)
}
if flags.file {
return writeTemporaryFile(replacements, printsep)
}
return strings.Join(replacements, " ")
})
}
func (t *Terminal) redraw(clear bool) {
if clear {
t.tui.Clear()
}
2017-04-28 13:58:08 +00:00
t.tui.Refresh()
t.printAll()
}
2017-01-27 08:46:56 +00:00
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool) {
valid, list := t.buildPlusList(template, forcePlus)
if !valid {
return
}
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
cmd := util.ExecCommand(command, false)
t.executing.Set(true)
2017-01-27 08:46:56 +00:00
if !background {
cmd.Stdin = tui.TtyIn()
2017-01-27 08:46:56 +00:00
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
2017-04-28 13:58:08 +00:00
t.tui.Pause(true)
2017-01-27 08:46:56 +00:00
cmd.Run()
t.tui.Resume(true, false)
t.redraw(true)
2017-01-27 08:46:56 +00:00
t.refresh()
} else {
t.tui.Pause(false)
2017-01-27 08:46:56 +00:00
cmd.Run()
t.tui.Resume(false, false)
2017-01-27 08:46:56 +00:00
}
t.executing.Set(false)
cleanTemporaryFiles()
}
func (t *Terminal) hasPreviewer() bool {
return t.previewBox != nil
}
func (t *Terminal) isPreviewEnabled() bool {
return t.hasPreviewer() && t.previewer.enabled
}
func (t *Terminal) hasPreviewWindow() bool {
return t.pwindow != nil && t.isPreviewEnabled()
}
func (t *Terminal) currentItem() *Item {
cnt := t.merger.Length()
if t.cy >= 0 && cnt > 0 && cnt > t.cy {
return t.merger.Get(t.cy).item
}
return nil
}
func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
current := t.currentItem()
slot, plus, query := hasPreviewFlags(template)
if !(!slot || query && len(t.input) > 0 || (forcePlus || plus) && len(t.selected) > 0) {
return current != nil, []*Item{current, current}
}
// We would still want to update preview window even if there is no match if
// 1. command template contains {q} and the query string is not empty
// 2. or it contains {+} and we have more than one item already selected.
// To do so, we pass an empty Item instead of nil to trigger an update.
if current == nil {
current = &minItem
}
var sels []*Item
if len(t.selected) == 0 {
sels = []*Item{current, current}
} else {
sels = make([]*Item, len(t.selected)+1)
sels[0] = current
for i, sel := range t.sortSelected() {
sels[i+1] = sel.item
}
}
return true, sels
}
func (t *Terminal) selectItem(item *Item) bool {
if len(t.selected) >= t.multi {
return false
}
if _, found := t.selected[item.Index()]; found {
2019-11-11 14:31:31 +00:00
return true
}
t.selected[item.Index()] = selectedItem{time.Now(), item}
t.version++
return true
}
func (t *Terminal) selectItemChanged(item *Item) bool {
if _, found := t.selected[item.Index()]; found {
return false
}
return t.selectItem(item)
}
func (t *Terminal) deselectItem(item *Item) {
delete(t.selected, item.Index())
t.version++
}
func (t *Terminal) deselectItemChanged(item *Item) bool {
if _, found := t.selected[item.Index()]; found {
t.deselectItem(item)
return true
}
return false
}
func (t *Terminal) toggleItem(item *Item) bool {
if _, found := t.selected[item.Index()]; !found {
return t.selectItem(item)
}
t.deselectItem(item)
return true
}
func (t *Terminal) killPreview(code int) {
select {
case t.killChan <- code:
default:
if code != exitCancel {
t.eventBox.Set(EvtQuit, code)
}
}
}
func (t *Terminal) cancelPreview() {
t.killPreview(exitCancel)
}
2015-01-11 18:01:24 +00:00
// Loop is called to start Terminal I/O
2015-01-01 19:49:30 +00:00
func (t *Terminal) Loop() {
2016-09-07 00:58:18 +00:00
// prof := profile.Start(profile.ProfilePath("/tmp/"))
<-t.startChan
2015-01-01 19:49:30 +00:00
{ // Late initialization
intChan := make(chan os.Signal, 1)
signal.Notify(intChan, os.Interrupt, syscall.SIGTERM)
go func() {
for s := range intChan {
// Don't quit by SIGINT while executing because it should be for the executing command and not for fzf itself
if !(s == os.Interrupt && t.executing.Get()) {
t.reqBox.Set(reqQuit, nil)
}
}
}()
2017-04-28 13:58:08 +00:00
contChan := make(chan os.Signal, 1)
notifyOnCont(contChan)
go func() {
for {
<-contChan
t.reqBox.Set(reqReinit, nil)
}
}()
2015-01-23 11:30:50 +00:00
resizeChan := make(chan os.Signal, 1)
notifyOnResize(resizeChan) // Non-portable
2015-01-23 11:30:50 +00:00
go func() {
for {
<-resizeChan
t.reqBox.Set(reqFullRedraw, nil)
2015-01-23 11:30:50 +00:00
}
}()
t.mutex.Lock()
t.initFunc()
t.resizeWindows()
t.printPrompt()
t.printInfo()
t.printHeader()
t.refresh()
t.mutex.Unlock()
go func() {
timer := time.NewTimer(t.initDelay)
<-timer.C
t.reqBox.Set(reqRefresh, nil)
}()
// Keep the spinner spinning
go func() {
for {
t.mutex.Lock()
reading := t.reading
t.mutex.Unlock()
time.Sleep(spinnerDuration)
if reading {
t.reqBox.Set(reqInfo, nil)
}
}
}()
2015-01-01 19:49:30 +00:00
}
if t.hasPreviewer() {
go func() {
2020-10-24 07:55:55 +00:00
var version int64
for {
var items []*Item
var commandTemplate string
var pwindow tui.Window
initialOffset := 0
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
switch req {
case reqPreviewEnqueue:
request := value.(previewRequest)
commandTemplate = request.template
initialOffset = request.scrollOffset
items = request.list
pwindow = request.pwindow
}
}
events.Clear()
})
version++
// We don't display preview window if no match
if items[0] != nil {
_, query := t.Input()
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
cmd := util.ExecCommand(command, true)
if pwindow != nil {
height := pwindow.Height()
env := os.Environ()
lines := fmt.Sprintf("LINES=%d", height)
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
env = append(env, lines)
env = append(env, "FZF_PREVIEW_"+lines)
env = append(env, columns)
env = append(env, "FZF_PREVIEW_"+columns)
cmd.Env = env
}
out, _ := cmd.StdoutPipe()
cmd.Stderr = cmd.Stdout
reader := bufio.NewReader(out)
eofChan := make(chan bool)
finishChan := make(chan bool, 1)
err := cmd.Start()
if err == nil {
reapChan := make(chan bool)
lineChan := make(chan eachLine)
// Goroutine 1 reads process output
go func() {
for {
line, err := reader.ReadString('\n')
lineChan <- eachLine{line, err}
if err != nil {
break
}
}
eofChan <- true
}()
// Goroutine 2 periodically requests rendering
rendered := util.NewAtomicBool(false)
2020-10-24 07:55:55 +00:00
go func(version int64) {
lines := []string{}
spinner := makeSpinner(t.unicode)
spinnerIndex := -1 // Delay initial rendering by an extra tick
ticker := time.NewTicker(previewChunkDelay)
offset := initialOffset
Loop:
for {
select {
case <-ticker.C:
if len(lines) > 0 && len(lines) >= initialOffset {
if spinnerIndex >= 0 {
spin := spinner[spinnerIndex%len(spinner)]
t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, spin})
rendered.Set(true)
offset = -1
}
spinnerIndex++
}
case eachLine := <-lineChan:
line := eachLine.line
err := eachLine.err
if len(line) > 0 {
clearIndex := strings.Index(line, clearCode)
if clearIndex >= 0 {
lines = []string{}
line = line[clearIndex+len(clearCode):]
version--
offset = 0
}
lines = append(lines, line)
}
if err != nil {
t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, ""})
rendered.Set(true)
break Loop
}
}
}
ticker.Stop()
reapChan <- true
}(version)
// Goroutine 3 is responsible for cancelling running preview command
go func(version int64) {
timer := time.NewTimer(previewDelayed)
Loop:
for {
select {
case <-timer.C:
t.reqBox.Set(reqPreviewDelayed, version)
case code := <-t.killChan:
if code != exitCancel {
util.KillCommand(cmd)
t.eventBox.Set(EvtQuit, code)
} else {
// We can immediately kill a long-running preview program
// once we started rendering its partial output
delay := previewCancelWait
if rendered.Get() {
delay = 0
}
timer := time.NewTimer(delay)
select {
case <-timer.C:
util.KillCommand(cmd)
case <-finishChan:
}
timer.Stop()
}
break Loop
case <-finishChan:
break Loop
}
}
timer.Stop()
reapChan <- true
}(version)
<-eofChan // Goroutine 1 finished
cmd.Wait() // NOTE: We should not call Wait before EOF
finishChan <- true // Tell Goroutine 3 to stop
<-reapChan // Goroutine 2 and 3 finished
<-reapChan
} else {
// Failed to start the command. Report the error immediately.
t.reqBox.Set(reqPreviewDisplay, previewResult{version, []string{err.Error()}, 0, ""})
}
cleanTemporaryFiles()
} else {
t.reqBox.Set(reqPreviewDisplay, previewResult{version, nil, 0, ""})
}
}
}()
}
refreshPreview := func(command string) {
if len(command) > 0 && t.isPreviewEnabled() {
_, list := t.buildPlusList(command, false)
2020-06-20 13:04:09 +00:00
t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list})
2020-06-20 13:04:09 +00:00
}
}
2015-01-01 19:49:30 +00:00
go func() {
var focusedIndex int32 = minItem.Index()
var version int64 = -1
2021-03-07 12:43:24 +00:00
running := true
2021-03-07 15:08:10 +00:00
code := exitError
exit := func(getCode func() int) {
t.tui.Close()
code = getCode()
if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input))
}
running = false
t.mutex.Unlock()
}
2021-03-07 12:43:24 +00:00
for running {
2015-01-12 03:56:17 +00:00
t.reqBox.Wait(func(events *util.Events) {
2015-01-01 19:49:30 +00:00
defer events.Clear()
t.mutex.Lock()
for req, value := range *events {
2015-01-01 19:49:30 +00:00
switch req {
2015-01-11 18:01:24 +00:00
case reqPrompt:
2015-01-01 19:49:30 +00:00
t.printPrompt()
if t.noInfoLine() {
2015-04-21 14:50:53 +00:00
t.printInfo()
}
2015-01-11 18:01:24 +00:00
case reqInfo:
2015-01-01 19:49:30 +00:00
t.printInfo()
2015-01-11 18:01:24 +00:00
case reqList:
2015-01-01 19:49:30 +00:00
t.printList()
var currentIndex int32 = minItem.Index()
currentItem := t.currentItem()
if currentItem != nil {
currentIndex = currentItem.Index()
}
if focusedIndex != currentIndex || version != t.version {
version = t.version
focusedIndex = currentIndex
2020-12-05 12:16:35 +00:00
refreshPreview(t.previewOpts.command)
}
case reqJump:
if t.merger.Length() == 0 {
t.jumping = jumpDisabled
}
t.printList()
2015-07-21 18:21:20 +00:00
case reqHeader:
t.printHeader()
2015-01-11 18:01:24 +00:00
case reqRefresh:
t.suppress = false
2017-04-28 13:58:08 +00:00
case reqReinit:
t.tui.Resume(t.fullscreen, t.sigstop)
t.redraw(true)
2015-01-11 18:01:24 +00:00
case reqRedraw:
t.redraw(false)
case reqFullRedraw:
t.redraw(true)
2015-01-11 18:01:24 +00:00
case reqClose:
2021-03-07 15:08:10 +00:00
exit(func() int {
if t.output() {
return exitOk
}
return exitNoMatch
})
return
case reqPreviewDisplay:
result := value.(previewResult)
2020-12-05 12:16:35 +00:00
if t.previewer.version != result.version {
t.previewer.version = result.version
t.previewer.following = t.previewOpts.follow
}
t.previewer.lines = result.lines
t.previewer.spinner = result.spinner
2020-12-05 12:16:35 +00:00
if t.previewer.following {
t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height()
} else if result.offset >= 0 {
t.previewer.offset = util.Constrain(result.offset, t.previewOpts.headerLines, len(t.previewer.lines)-1)
}
t.printPreview()
case reqPreviewRefresh:
t.printPreview()
case reqPreviewDelayed:
2020-10-24 07:55:55 +00:00
t.previewer.version = value.(int64)
t.printPreviewDelayed()
case reqPrintQuery:
2021-03-07 15:08:10 +00:00
exit(func() int {
t.printer(string(t.input))
return exitOk
})
return
2015-01-11 18:01:24 +00:00
case reqQuit:
2021-03-07 15:08:10 +00:00
exit(func() int { return exitInterrupt })
return
2015-01-01 19:49:30 +00:00
}
}
t.refresh()
2015-01-01 19:49:30 +00:00
t.mutex.Unlock()
})
}
2021-03-07 15:08:10 +00:00
// prof.Stop()
t.killPreview(code)
2015-01-01 19:49:30 +00:00
}()
looping := true
for looping {
var newCommand *string
changed := false
2020-06-07 14:07:03 +00:00
beof := false
queryChanged := false
2017-01-07 16:30:31 +00:00
event := t.tui.GetChar()
2015-01-01 19:49:30 +00:00
t.mutex.Lock()
previousInput := t.input
2019-03-29 06:02:31 +00:00
previousCx := t.cx
events := []util.EventType{}
2015-01-12 03:56:17 +00:00
req := func(evts ...util.EventType) {
2015-01-01 19:49:30 +00:00
for _, event := range evts {
events = append(events, event)
2015-01-11 18:01:24 +00:00
if event == reqClose || event == reqQuit {
2015-01-01 19:49:30 +00:00
looping = false
}
}
}
togglePreview := func(enabled bool) bool {
if t.previewer.enabled != enabled {
t.previewer.enabled = enabled
// We need to immediately update t.pwindow so we don't use reqRedraw
t.resizeWindows()
req(reqPrompt, reqList, reqInfo, reqHeader)
return true
}
return false
}
toggle := func() bool {
current := t.currentItem()
if current != nil && t.toggleItem(current) {
2015-01-11 18:01:24 +00:00
req(reqInfo)
return true
}
return false
}
scrollPreviewTo := func(newOffset int) {
if !t.previewer.scrollable {
return
}
2020-12-05 12:16:35 +00:00
t.previewer.following = false
numLines := len(t.previewer.lines)
2020-12-05 12:16:35 +00:00
if t.previewOpts.cycle {
newOffset = (newOffset + numLines) % numLines
}
newOffset = util.Constrain(newOffset, t.previewOpts.headerLines, numLines-1)
if t.previewer.offset != newOffset {
t.previewer.offset = newOffset
req(reqPreviewRefresh)
}
}
scrollPreviewBy := func(amount int) {
scrollPreviewTo(t.previewer.offset + amount)
}
for key, ret := range t.expect {
2015-03-31 13:05:02 +00:00
if keyMatch(key, event) {
t.pressed = ret
t.reqBox.Set(reqClose, nil)
t.mutex.Unlock()
return
}
}
2015-05-20 12:25:15 +00:00
actionsFor := func(eventType tui.EventType) []*action {
return t.keymap[eventType.AsEvent()]
}
var doAction func(*action) bool
doActions := func(actions []*action) bool {
for _, action := range actions {
if !doAction(action) {
return false
}
}
return true
}
doAction = func(a *action) bool {
switch a.t {
2015-10-12 17:24:38 +00:00
case actIgnore:
2017-01-27 08:46:56 +00:00
case actExecute, actExecuteSilent:
t.executeCommand(a.a, false, a.t == actExecuteSilent)
2015-11-08 16:42:01 +00:00
case actExecuteMulti:
2017-01-27 08:46:56 +00:00
t.executeCommand(a.a, true, false)
2015-10-12 17:24:38 +00:00
case actInvalid:
t.mutex.Unlock()
return false
case actTogglePreview:
if t.hasPreviewer() {
togglePreview(!t.previewer.enabled)
if t.previewer.enabled {
2020-12-05 12:16:35 +00:00
valid, list := t.buildPlusList(t.previewOpts.command, false)
if valid {
t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue,
previewRequest{t.previewOpts.command, t.pwindow, t.evaluateScrollOffset(), list})
}
}
}
2017-02-18 14:49:00 +00:00
case actTogglePreviewWrap:
if t.hasPreviewWindow() {
2020-12-05 12:16:35 +00:00
t.previewOpts.wrap = !t.previewOpts.wrap
// Reset preview version so that full redraw occurs
t.previewed.version = 0
2017-02-18 14:49:00 +00:00
req(reqPreviewRefresh)
}
2015-10-12 17:24:38 +00:00
case actToggleSort:
t.sort = !t.sort
changed = true
case actPreviewTop:
if t.hasPreviewWindow() {
scrollPreviewTo(0)
}
case actPreviewBottom:
if t.hasPreviewWindow() {
scrollPreviewTo(len(t.previewer.lines) - t.pwindow.Height())
}
case actPreviewUp:
if t.hasPreviewWindow() {
scrollPreviewBy(-1)
}
case actPreviewDown:
if t.hasPreviewWindow() {
scrollPreviewBy(1)
}
case actPreviewPageUp:
if t.hasPreviewWindow() {
scrollPreviewBy(-t.pwindow.Height())
}
case actPreviewPageDown:
if t.hasPreviewWindow() {
scrollPreviewBy(t.pwindow.Height())
}
case actPreviewHalfPageUp:
if t.hasPreviewWindow() {
scrollPreviewBy(-t.pwindow.Height() / 2)
}
case actPreviewHalfPageDown:
if t.hasPreviewWindow() {
scrollPreviewBy(t.pwindow.Height() / 2)
}
2015-10-12 17:24:38 +00:00
case actBeginningOfLine:
2015-07-23 12:05:33 +00:00
t.cx = 0
2015-10-12 17:24:38 +00:00
case actBackwardChar:
if t.cx > 0 {
t.cx--
}
case actPrintQuery:
req(reqPrintQuery)
2020-12-04 11:34:41 +00:00
case actChangePrompt:
t.prompt, t.promptLen = t.parsePrompt(a.a)
req(reqPrompt)
case actPreview:
togglePreview(true)
refreshPreview(a.a)
2020-06-20 13:04:09 +00:00
case actRefreshPreview:
2020-12-05 12:16:35 +00:00
refreshPreview(t.previewOpts.command)
case actReplaceQuery:
current := t.currentItem()
if current != nil {
t.input = current.text.ToRunes()
t.cx = len(t.input)
}
2015-10-12 17:24:38 +00:00
case actAbort:
req(reqQuit)
case actDeleteChar:
t.delChar()
case actDeleteCharEOF:
if !t.delChar() && t.cx == 0 {
req(reqQuit)
}
2015-10-12 17:24:38 +00:00
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 actBackwardDeleteCharEOF:
if len(t.input) == 0 {
req(reqQuit)
} else if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
t.cx--
}
2015-10-12 17:24:38 +00:00
case actForwardChar:
if t.cx < len(t.input) {
t.cx++
}
case actBackwardDeleteChar:
2020-06-07 14:07:03 +00:00
beof = len(t.input) == 0
2015-10-12 17:24:38 +00:00
if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
t.cx--
}
case actSelectAll:
if t.multi > 0 {
2015-10-12 17:24:38 +00:00
for i := 0; i < t.merger.Length(); i++ {
if !t.selectItem(t.merger.Get(i).item) {
break
}
2015-10-12 17:24:38 +00:00
}
req(reqList, reqInfo)
}
case actDeselectAll:
if t.multi > 0 {
for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ {
2019-11-02 10:55:05 +00:00
t.deselectItem(t.merger.Get(i).item)
}
2015-10-12 17:24:38 +00:00
req(reqList, reqInfo)
}
2021-02-01 15:08:54 +00:00
case actClose:
if t.isPreviewEnabled() {
togglePreview(false)
} else {
req(reqQuit)
}
case actSelect:
current := t.currentItem()
if t.multi > 0 && current != nil && t.selectItemChanged(current) {
req(reqList, reqInfo)
}
case actDeselect:
current := t.currentItem()
if t.multi > 0 && current != nil && t.deselectItemChanged(current) {
req(reqList, reqInfo)
}
2015-10-12 17:24:38 +00:00
case actToggle:
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
2015-10-12 17:24:38 +00:00
req(reqList)
}
case actToggleAll:
if t.multi > 0 {
prevIndexes := make(map[int]struct{})
for i := 0; i < t.merger.Length() && len(t.selected) > 0; i++ {
item := t.merger.Get(i).item
if _, found := t.selected[item.Index()]; found {
prevIndexes[i] = struct{}{}
t.deselectItem(item)
}
}
2015-10-12 17:24:38 +00:00
for i := 0; i < t.merger.Length(); i++ {
if _, found := prevIndexes[i]; !found {
item := t.merger.Get(i).item
if !t.selectItem(item) {
break
}
}
2015-10-12 17:24:38 +00:00
}
req(reqList, reqInfo)
}
case actToggleIn:
if t.layout != layoutDefault {
return doAction(&action{t: actToggleUp})
}
return doAction(&action{t: actToggleDown})
case actToggleOut:
if t.layout != layoutDefault {
return doAction(&action{t: actToggleDown})
}
return doAction(&action{t: actToggleUp})
2015-10-12 17:24:38 +00:00
case actToggleDown:
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
t.vmove(-1, true)
2015-10-12 17:24:38 +00:00
req(reqList)
}
case actToggleUp:
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
t.vmove(1, true)
2015-10-12 17:24:38 +00:00
req(reqList)
}
case actDown:
t.vmove(-1, true)
2015-01-11 18:01:24 +00:00
req(reqList)
2015-10-12 17:24:38 +00:00
case actUp:
t.vmove(1, true)
2015-01-11 18:01:24 +00:00
req(reqList)
2015-10-12 17:24:38 +00:00
case actAccept:
req(reqClose)
case actAcceptNonEmpty:
if len(t.selected) > 0 || t.merger.Length() > 0 || !t.reading && t.count == 0 {
req(reqClose)
}
2015-10-12 17:24:38 +00:00
case actClearScreen:
req(reqFullRedraw)
case actClearQuery:
t.input = []rune{}
t.cx = 0
case actClearSelection:
if t.multi > 0 {
t.selected = make(map[int32]selectedItem)
t.version++
req(reqList, reqInfo)
}
case actFirst:
t.vset(0)
req(reqList)
case actLast:
t.vset(t.merger.Length() - 1)
req(reqList)
2015-10-12 17:24:38 +00:00
case actUnixLineDiscard:
2020-06-07 14:07:03 +00:00
beof = len(t.input) == 0
2015-10-12 17:24:38 +00:00
if t.cx > 0 {
t.yanked = copySlice(t.input[:t.cx])
t.input = t.input[t.cx:]
t.cx = 0
2015-07-26 15:06:44 +00:00
}
2015-10-12 17:24:38 +00:00
case actUnixWordRubout:
2020-06-07 14:07:03 +00:00
beof = len(t.input) == 0
2015-10-12 17:24:38 +00:00
if t.cx > 0 {
t.rubout("\\s\\S")
2015-01-01 19:49:30 +00:00
}
2015-10-12 17:24:38 +00:00
case actBackwardKillWord:
2020-06-07 14:07:03 +00:00
beof = len(t.input) == 0
2015-10-12 17:24:38 +00:00
if t.cx > 0 {
2017-01-15 10:42:28 +00:00
t.rubout(t.wordRubout)
2015-01-01 19:49:30 +00:00
}
2015-10-12 17:24:38 +00:00
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, false)
2015-10-12 17:24:38 +00:00
req(reqList)
case actPageDown:
t.vmove(-(t.maxItems() - 1), false)
2015-10-12 17:24:38 +00:00
req(reqList)
2017-01-16 02:58:13 +00:00
case actHalfPageUp:
t.vmove(t.maxItems()/2, false)
2017-01-16 02:58:13 +00:00
req(reqList)
case actHalfPageDown:
t.vmove(-(t.maxItems() / 2), false)
2017-01-16 02:58:13 +00:00
req(reqList)
case actJump:
t.jumping = jumpEnabled
req(reqJump)
case actJumpAccept:
t.jumping = jumpAcceptEnabled
req(reqJump)
2015-10-12 17:24:38 +00:00
case actBackwardWord:
2017-01-15 10:42:28 +00:00
t.cx = findLastMatch(t.wordRubout, string(t.input[:t.cx])) + 1
2015-10-12 17:24:38 +00:00
case actForwardWord:
2017-01-15 10:42:28 +00:00
t.cx += findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1
2015-10-12 17:24:38 +00:00
case actKillWord:
ncx := t.cx +
2017-01-15 10:42:28 +00:00
findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1
2015-10-12 17:24:38 +00:00
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 = trimQuery(t.history.previous())
2015-10-12 17:24:38 +00:00
t.cx = len(t.input)
}
case actNextHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = trimQuery(t.history.next())
2015-10-12 17:24:38 +00:00
t.cx = len(t.input)
}
case actToggleSearch:
t.paused = !t.paused
changed = !t.paused
req(reqPrompt)
case actEnableSearch:
t.paused = false
changed = true
req(reqPrompt)
case actDisableSearch:
t.paused = true
req(reqPrompt)
2017-04-28 13:58:08 +00:00
case actSigStop:
p, err := os.FindProcess(os.Getpid())
if err == nil {
t.sigstop = true
2017-04-28 13:58:08 +00:00
t.tui.Clear()
t.tui.Pause(t.fullscreen)
notifyStop(p)
t.mutex.Unlock()
return false
}
2015-10-12 17:24:38 +00:00
case actMouse:
me := event.MouseEvent
mx, my := me.X, me.Y
if me.S != 0 {
// Scroll
if t.window.Enclose(my, mx) && t.merger.Length() > 0 {
if t.multi > 0 && me.Mod {
2015-07-26 14:02:04 +00:00
toggle()
}
t.vmove(me.S, true)
2015-07-26 14:02:04 +00:00
req(reqList)
} else if t.hasPreviewWindow() && t.pwindow.Enclose(my, mx) {
scrollPreviewBy(-me.S)
2015-01-01 19:49:30 +00:00
}
} else if t.window.Enclose(my, mx) {
2017-01-07 16:30:31 +00:00
mx -= t.window.Left()
my -= t.window.Top()
mx = util.Constrain(mx-t.promptLen, 0, len(t.input))
2015-10-12 17:24:38 +00:00
min := 2 + len(t.header)
if t.noInfoLine() {
2015-10-12 17:24:38 +00:00
min--
}
h := t.window.Height()
switch t.layout {
case layoutDefault:
my = h - my - 1
case layoutReverseList:
if my < h-min {
my += min
} else {
my = h - my - 1
}
}
2015-10-12 17:24:38 +00:00
if me.Double {
// Double-click
if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
return doActions(actionsFor(tui.DoubleClick))
2015-10-12 17:24:38 +00:00
}
}
} else if me.Down {
if my == t.promptLine() && mx >= 0 {
2015-10-12 17:24:38 +00:00
// Prompt
t.cx = mx + t.xoffset
2015-10-12 17:24:38 +00:00
} else if my >= min {
// List
if t.vset(t.offset+my-min) && t.multi > 0 && me.Mod {
2015-10-12 17:24:38 +00:00
toggle()
}
req(reqList)
if me.Left {
return doActions(actionsFor(tui.LeftClick))
}
return doActions(actionsFor(tui.RightClick))
2015-10-12 17:24:38 +00:00
}
}
2015-01-01 19:49:30 +00:00
}
case actReload:
t.failed = nil
valid, list := t.buildPlusList(a.a, false)
if !valid {
// We run the command even when there's no match
// 1. If the template doesn't have any slots
// 2. If the template has {q}
slot, _, query := hasPreviewFlags(a.a)
valid = !slot || query
}
if valid {
command := t.replacePlaceholder(a.a, false, string(t.input), list)
newCommand = &command
t.reading = true
t.version++
}
2021-05-22 04:13:55 +00:00
case actUnbind:
keys := parseKeyChords(a.a, "PANIC")
for key := range keys {
delete(t.keymap, key)
}
case actChangePreview:
if t.previewOpts.command != a.a {
togglePreview(len(a.a) > 0)
t.previewOpts.command = a.a
refreshPreview(t.previewOpts.command)
}
case actChangePreviewWindow:
currentPreviewOpts := t.previewOpts
// Reset preview options and apply the additional options
t.previewOpts = t.initialPreviewOpts
// Split window options
tokens := strings.Split(a.a, "|")
parsePreviewWindow(&t.previewOpts, tokens[0])
if len(tokens) > 1 {
a.a = strings.Join(append(tokens[1:], tokens[0]), "|")
}
if t.previewOpts.hidden {
togglePreview(false)
} else {
// Full redraw
if !currentPreviewOpts.sameLayout(t.previewOpts) {
if togglePreview(true) {
refreshPreview(t.previewOpts.command)
} else {
req(reqRedraw)
}
} else if !currentPreviewOpts.sameContentLayout(t.previewOpts) {
t.previewed.version = 0
req(reqPreviewRefresh)
}
// Adjust scroll offset
if t.hasPreviewWindow() && currentPreviewOpts.scroll != t.previewOpts.scroll {
scrollPreviewTo(t.evaluateScrollOffset())
}
}
2015-01-01 19:49:30 +00:00
}
2015-10-12 17:24:38 +00:00
return true
}
if t.jumping == jumpDisabled {
actions := t.keymap[event.Comparable()]
if len(actions) == 0 && event.Type == tui.Rune {
doAction(&action{t: actRune})
} else if !doActions(actions) {
continue
}
2017-02-18 14:17:29 +00:00
t.truncateQuery()
queryChanged = string(previousInput) != string(t.input)
changed = changed || queryChanged
if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs {
if !doActions(onChanges) {
continue
}
}
if onEOFs, prs := t.keymap[tui.BackwardEOF.AsEvent()]; beof && prs {
if !doActions(onEOFs) {
2020-06-07 14:07:03 +00:00
continue
}
}
} else {
if event.Type == tui.Rune {
if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() {
t.cy = idx + t.offset
if t.jumping == jumpAcceptEnabled {
req(reqClose)
}
}
}
t.jumping = jumpDisabled
req(reqList)
}
2015-01-01 19:49:30 +00:00
if queryChanged {
if t.isPreviewEnabled() {
2020-12-05 12:16:35 +00:00
_, _, q := hasPreviewFlags(t.previewOpts.command)
if q {
t.version++
}
}
2015-01-01 19:49:30 +00:00
}
2019-03-29 06:02:31 +00:00
if queryChanged || t.cx != previousCx {
2019-03-29 06:02:31 +00:00
req(reqPrompt)
}
t.mutex.Unlock() // Must be unlocked before touching reqBox
2019-03-29 17:06:54 +00:00
if changed || newCommand != nil {
t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, command: newCommand})
2019-03-29 17:06:54 +00:00
}
2015-01-01 19:49:30 +00:00
for _, event := range events {
t.reqBox.Set(event, nil)
}
}
}
func (t *Terminal) constrain() {
// count of items to display allowed by filtering
count := t.merger.Length()
// count of lines can be displayed
2015-04-21 14:50:53 +00:00
height := t.maxItems()
2015-01-01 19:49:30 +00:00
2015-01-12 03:56:17 +00:00
t.cy = util.Constrain(t.cy, 0, count-1)
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
}
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
}
}
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) vmove(o int, allowCycle bool) {
if t.layout != layoutDefault {
o *= -1
}
dest := t.cy + o
if t.cycle && allowCycle {
max := t.merger.Length() - 1
if dest > max {
if t.cy == max {
dest = 0
}
} else if dest < 0 {
if t.cy == 0 {
dest = max
}
}
2015-01-01 19:49:30 +00:00
}
t.vset(dest)
2015-01-10 05:50:24 +00:00
}
func (t *Terminal) vset(o int) bool {
2015-01-12 03:56:17 +00:00
t.cy = util.Constrain(o, 0, t.merger.Length()-1)
2015-01-10 05:50:24 +00:00
return t.cy == o
2015-01-01 19:49:30 +00:00
}
2015-04-21 14:50:53 +00:00
func (t *Terminal) maxItems() int {
2017-01-07 16:30:31 +00:00
max := t.window.Height() - 2 - len(t.header)
if t.noInfoLine() {
2015-08-02 04:06:15 +00:00
max++
2015-04-21 14:50:53 +00:00
}
return util.Max(max, 0)
2015-01-01 19:49:30 +00:00
}