fzf/src/terminal.go

1877 lines
44 KiB
Go
Raw Normal View History

2015-01-01 19:49:30 +00:00
package fzf
import (
"bufio"
2015-01-18 07:59:04 +00:00
"bytes"
2015-01-01 19:49:30 +00:00
"fmt"
"io"
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/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"
var placeholder *regexp.Regexp
func init() {
placeholder = regexp.MustCompile("\\\\?(?:{\\+?[0-9,-.]*}|{q})")
}
type jumpMode int
const (
jumpDisabled jumpMode = iota
jumpEnabled
jumpAcceptEnabled
)
type previewer struct {
text string
lines int
offset int
enabled bool
}
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
2015-04-21 14:50:53 +00:00
inlineInfo bool
2015-01-01 19:49:30 +00:00
prompt string
promptLen int
2015-01-01 19:49:30 +00:00
reverse bool
2017-04-28 13:58:08 +00:00
fullscreen bool
hscroll bool
hscrollOff int
2017-01-15 10:42:28 +00:00
wordRubout string
wordNext string
2015-01-01 19:49:30 +00:00
cx int
cy int
offset int
yanked []rune
input []rune
multi bool
sort bool
2015-05-20 12:25:15 +00:00
toggleSort bool
delimiter Delimiter
expect map[int]string
keymap map[int][]action
pressed string
2015-01-01 19:49:30 +00:00
printQuery bool
history *History
cycle bool
2015-07-21 15:19:37 +00:00
header []string
header0 []string
ansi bool
2017-01-07 16:30:31 +00:00
tabstop int
margin [4]sizeSpec
2016-11-15 14:57:32 +00:00
strong tui.Attr
bordered bool
cleanExit bool
border tui.Window
2017-01-07 16:30:31 +00:00
window tui.Window
pborder tui.Window
2017-01-07 16:30:31 +00:00
pwindow tui.Window
2015-01-01 19:49:30 +00:00
count int
progress int
reading bool
success bool
jumping jumpMode
jumpLabels string
2016-09-17 19:52:47 +00:00
printer func(string)
merger *Merger
selected map[int32]selectedItem
version int64
2015-01-12 03:56:17 +00:00
reqBox *util.EventBox
preview previewOpts
previewer previewer
previewBox *util.EventBox
2015-01-12 03:56:17 +00:00
eventBox *util.EventBox
2015-01-01 19:49:30 +00:00
mutex sync.Mutex
initFunc func()
2017-01-07 16:30:31 +00:00
prevLines []itemLine
suppress bool
startChan chan bool
2016-09-07 00:58:18 +00:00
slab *util.Slab
theme *tui.ColorTheme
2017-01-07 16:30:31 +00:00
tui tui.Renderer
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-11 18:01:24 +00:00
var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
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
reqClose
reqPrintQuery
reqPreviewEnqueue
reqPreviewDisplay
reqPreviewRefresh
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
actBackwardChar
actBackwardDeleteChar
actBackwardWord
2015-07-23 12:05:33 +00:00
actCancel
2015-05-20 12:25:15 +00:00
actClearScreen
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
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
2015-05-20 12:25:15 +00:00
actToggleSort
actTogglePreview
2017-02-18 14:49:00 +00:00
actTogglePreviewWrap
actPreviewUp
actPreviewDown
actPreviewPageUp
actPreviewPageDown
actPreviousHistory
actNextHistory
actExecute
2017-01-27 08:46:56 +00:00
actExecuteSilent
actExecuteMulti // Deprecated
2017-04-28 13:58:08 +00:00
actSigStop
actTop
2015-05-20 12:25:15 +00:00
)
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[int][]action {
keymap := make(map[int][]action)
keymap[tui.Invalid] = toActions(actInvalid)
keymap[tui.Resize] = toActions(actClearScreen)
keymap[tui.CtrlA] = toActions(actBeginningOfLine)
keymap[tui.CtrlB] = toActions(actBackwardChar)
keymap[tui.CtrlC] = toActions(actAbort)
keymap[tui.CtrlG] = toActions(actAbort)
keymap[tui.CtrlQ] = toActions(actAbort)
keymap[tui.ESC] = toActions(actAbort)
keymap[tui.CtrlD] = toActions(actDeleteCharEOF)
keymap[tui.CtrlE] = toActions(actEndOfLine)
keymap[tui.CtrlF] = toActions(actForwardChar)
keymap[tui.CtrlH] = toActions(actBackwardDeleteChar)
keymap[tui.BSpace] = toActions(actBackwardDeleteChar)
keymap[tui.Tab] = toActions(actToggleDown)
keymap[tui.BTab] = toActions(actToggleUp)
keymap[tui.CtrlJ] = toActions(actDown)
keymap[tui.CtrlK] = toActions(actUp)
keymap[tui.CtrlL] = toActions(actClearScreen)
keymap[tui.CtrlM] = toActions(actAccept)
keymap[tui.CtrlN] = toActions(actDown)
keymap[tui.CtrlP] = toActions(actUp)
keymap[tui.CtrlU] = toActions(actUnixLineDiscard)
keymap[tui.CtrlW] = toActions(actUnixWordRubout)
keymap[tui.CtrlY] = toActions(actYank)
2017-04-28 13:58:08 +00:00
if !util.IsWindows() {
keymap[tui.CtrlZ] = toActions(actSigStop)
}
keymap[tui.AltB] = toActions(actBackwardWord)
keymap[tui.SLeft] = toActions(actBackwardWord)
keymap[tui.AltF] = toActions(actForwardWord)
keymap[tui.SRight] = toActions(actForwardWord)
keymap[tui.AltD] = toActions(actKillWord)
keymap[tui.AltBS] = toActions(actBackwardKillWord)
keymap[tui.Up] = toActions(actUp)
keymap[tui.Down] = toActions(actDown)
keymap[tui.Left] = toActions(actBackwardChar)
keymap[tui.Right] = toActions(actForwardChar)
keymap[tui.Home] = toActions(actBeginningOfLine)
keymap[tui.End] = toActions(actEndOfLine)
keymap[tui.Del] = toActions(actDeleteChar)
keymap[tui.PgUp] = toActions(actPageUp)
keymap[tui.PgDn] = toActions(actPageDown)
keymap[tui.Rune] = toActions(actRune)
keymap[tui.Mouse] = toActions(actMouse)
keymap[tui.DoubleClick] = toActions(actAccept)
keymap[tui.LeftClick] = toActions(actIgnore)
keymap[tui.RightClick] = toActions(actToggle)
2015-05-20 12:25:15 +00:00
return keymap
}
func trimQuery(query string) []rune {
return []rune(strings.Replace(query, "\t", " ", -1))
}
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
if opts.Reverse {
header = opts.Header
} else {
header = reverseStringArray(opts.Header)
}
var delay time.Duration
if opts.Tac {
delay = initialDelayTac
} else {
delay = initialDelay
}
var previewBox *util.EventBox
if len(opts.Preview.command) > 0 {
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.InlineInfo {
effectiveMinHeight -= 1
}
if opts.Bordered {
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
}
2017-01-15 10:42:28 +00:00
wordRubout := "[^[:alnum:]][[:alnum:]]"
wordNext := "[[:alnum:]][^[:alnum:]]|(.$)"
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,
2015-04-21 14:50:53 +00:00
inlineInfo: opts.InlineInfo,
2015-01-01 19:49:30 +00:00
reverse: opts.Reverse,
2017-04-28 13:58:08 +00:00
fullscreen: fullscreen,
hscroll: opts.Hscroll,
hscrollOff: opts.HscrollOff,
2017-01-15 10:42:28 +00:00
wordRubout: wordRubout,
wordNext: wordNext,
cx: len(input),
2015-01-01 19:49:30 +00:00
cy: 0,
offset: 0,
yanked: []rune{},
input: input,
multi: opts.Multi,
sort: opts.Sort > 0,
2015-03-31 13:05:02 +00:00
toggleSort: opts.ToggleSort,
delimiter: opts.Delimiter,
expect: opts.Expect,
2015-05-20 12:25:15 +00:00
keymap: opts.Keymap,
pressed: "",
2015-01-01 19:49:30 +00:00
printQuery: opts.PrintQuery,
history: opts.History,
2015-07-26 14:02:04 +00:00
margin: opts.Margin,
bordered: opts.Bordered,
cleanExit: opts.ClearOnExit,
2016-11-15 14:57:32 +00:00
strong: strongAttr,
cycle: opts.Cycle,
header: header,
header0: header,
ansi: opts.Ansi,
2017-01-07 16:30:31 +00:00
tabstop: opts.Tabstop,
reading: true,
success: true,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
2016-09-17 19:52:47 +00:00
printer: opts.Printer,
merger: EmptyMerger,
selected: make(map[int32]selectedItem),
2015-01-12 03:56:17 +00:00
reqBox: util.NewEventBox(),
preview: opts.Preview,
previewer: previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden},
previewBox: previewBox,
2015-01-01 19:49:30 +00:00
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
2016-09-07 00:58:18 +00:00
slab: util.MakeSlab(slab16Size, slab32Size),
theme: opts.Theme,
startChan: make(chan bool, 1),
2017-01-07 16:30:31 +00:00
tui: renderer,
initFunc: func() { renderer.Init() }}
t.prompt, t.promptLen = t.processTabs([]rune(opts.Prompt), 0)
return &t
2015-01-01 19:49:30 +00:00
}
2015-01-11 18:01:24 +00:00
// Input returns current query string
2015-01-01 19:49:30 +00:00
func (t *Terminal) Input() []rune {
t.mutex.Lock()
defer t.mutex.Unlock()
return copySlice(t.input)
}
2015-01-11 18:01:24 +00:00
// UpdateCount updates the count information
func (t *Terminal) UpdateCount(cnt int, final bool, success bool) {
2015-01-01 19:49:30 +00:00
t.mutex.Lock()
t.count = cnt
t.reading = !final
t.success = success
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) {
2015-01-01 19:49:30 +00:00
t.mutex.Lock()
t.progress = 100
t.merger = merger
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 {
2015-01-01 19:49:30 +00:00
l := 0
for _, r := range runes {
2017-01-07 16:30:31 +00:00
l += util.RuneWidth(r, l, t.tabstop)
2015-01-01 19:49:30 +00:00
}
return l
}
const (
minWidth = 16
minHeight = 4
maxDisplayWidthCalc = 1024
)
2015-07-26 14:02:04 +00:00
func calculateSize(base int, size sizeSpec, margin int, minSize int) int {
max := base - margin
if size.percent {
return util.Constrain(int(float64(base)*0.01*size.size), minSize, max)
}
return util.Constrain(int(size.size), minSize, max)
}
func (t *Terminal) resizeWindows() {
2017-01-07 16:30:31 +00:00
screenWidth := t.tui.MaxX()
screenHeight := t.tui.MaxY()
marginInt := [4]int{}
2017-01-07 16:30:31 +00:00
t.prevLines = make([]itemLine, screenHeight)
for idx, sizeSpec := range t.margin {
if sizeSpec.percent {
var max float64
2015-07-26 14:02:04 +00:00
if idx%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
}
marginInt[idx] = int(max * sizeSpec.size * 0.01)
2015-07-26 14:02:04 +00:00
} else {
marginInt[idx] = int(sizeSpec.size)
2015-07-26 14:02:04 +00:00
}
if t.bordered && idx%2 == 0 {
marginInt[idx] += 1
}
2015-07-26 14:02:04 +00:00
}
adjust := func(idx1 int, idx2 int, max int, min int) {
if max >= min {
margin := marginInt[idx1] + marginInt[idx2]
2015-07-26 14:02:04 +00:00
if max-margin < min {
desired := max - min
marginInt[idx1] = desired * marginInt[idx1] / margin
marginInt[idx2] = desired * marginInt[idx2] / margin
2015-07-26 14:02:04 +00:00
}
}
}
previewVisible := t.isPreviewEnabled() && t.preview.size.size > 0
minAreaWidth := minWidth
minAreaHeight := minHeight
if previewVisible {
switch t.preview.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()
t.pwindow.Close()
}
width := screenWidth - marginInt[1] - marginInt[3]
height := screenHeight - marginInt[0] - marginInt[2]
if t.bordered {
t.border = t.tui.NewWindow(
marginInt[0]-1,
marginInt[3],
width,
height+2, tui.BorderHorizontal)
}
if previewVisible {
createPreviewWindow := func(y int, x int, w int, h int) {
t.pborder = t.tui.NewWindow(y, x, w, h, tui.BorderAround)
pwidth := w - 4
// ncurses auto-wraps the line when the cursor reaches the right-end of
// the window. To prevent unintended line-wraps, we use the width one
// column larger than the desired value.
2017-01-07 16:30:31 +00:00
if !t.preview.wrap && t.tui.DoesAutoWrap() {
pwidth += 1
}
t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, tui.BorderNone)
os.Setenv("FZF_PREVIEW_HEIGHT", strconv.Itoa(h-2))
}
switch t.preview.position {
case posUp:
pheight := calculateSize(height, t.preview.size, minHeight, 3)
2017-01-07 16:30:31 +00:00
t.window = t.tui.NewWindow(
marginInt[0]+pheight, marginInt[3], width, height-pheight, tui.BorderNone)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
case posDown:
pheight := calculateSize(height, t.preview.size, minHeight, 3)
2017-01-07 16:30:31 +00:00
t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width, height-pheight, tui.BorderNone)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
case posLeft:
pwidth := calculateSize(width, t.preview.size, minWidth, 5)
2017-01-07 16:30:31 +00:00
t.window = t.tui.NewWindow(
marginInt[0], marginInt[3]+pwidth, width-pwidth, height, tui.BorderNone)
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
case posRight:
pwidth := calculateSize(width, t.preview.size, minWidth, 5)
2017-01-07 16:30:31 +00:00
t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width-pwidth, height, tui.BorderNone)
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, tui.BorderNone)
}
for i := 0; i < t.window.Height(); i++ {
t.window.MoveAndClear(i, 0)
}
2017-02-18 14:17:29 +00:00
t.truncateQuery()
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) {
if !t.reverse {
2017-01-07 16:30:31 +00:00
y = t.window.Height() - y - 1
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) placeCursor() {
t.move(0, t.promptLen+t.displayWidth(t.input[:t.cx]), false)
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) printPrompt() {
t.move(0, 0, true)
2016-11-15 14:57:32 +00:00
t.window.CPrint(tui.ColPrompt, t.strong, t.prompt)
t.window.CPrint(tui.ColNormal, t.strong, string(t.input))
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) printInfo() {
2017-02-18 14:17:29 +00:00
pos := 0
2015-04-21 14:50:53 +00:00
if t.inlineInfo {
pos = t.promptLen + t.displayWidth(t.input) + 1
2017-02-18 14:17:29 +00:00
if pos+len(" < ") > t.window.Width() {
return
}
t.move(0, pos, true)
2015-04-21 14:50:53 +00:00
if t.reading {
2016-11-15 14:57:32 +00:00
t.window.CPrint(tui.ColSpinner, t.strong, " < ")
2015-04-21 14:50:53 +00:00
} else {
2016-11-15 14:57:32 +00:00
t.window.CPrint(tui.ColPrompt, t.strong, " < ")
2015-04-21 14:50:53 +00:00
}
2017-02-18 14:17:29 +00:00
pos += len(" < ")
2015-04-21 14:50:53 +00:00
} else {
t.move(1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
2016-11-15 14:57:32 +00:00
t.window.CPrint(tui.ColSpinner, t.strong, _spinner[idx])
2015-04-21 14:50:53 +00:00
}
t.move(1, 2, false)
2017-02-18 14:17:29 +00:00
pos = 2
2015-01-01 19:49:30 +00:00
}
output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count)
2015-05-20 12:25:15 +00:00
if t.toggleSort {
if t.sort {
output += " +S"
} else {
output += " -S"
}
}
2015-01-01 19:49:30 +00:00
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)
}
if !t.success && t.count == 0 {
if len(os.Getenv("FZF_DEFAULT_COMMAND")) > 0 {
output = "[$FZF_DEFAULT_COMMAND failed]"
} else {
output = "[default command failed - $FZF_DEFAULT_COMMAND required]"
}
}
2017-02-18 14:17:29 +00:00
if pos+len(output) <= t.window.Width() {
t.window.CPrint(tui.ColInfo, 0, 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()
var state *ansiState
2015-07-21 15:19:37 +00:00
for idx, lineStr := range t.header {
line := idx + 2
if t.inlineInfo {
2015-08-02 04:06:15 +00:00
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.AttrRegular, 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.reverse {
i = maxy - 1 - j
}
2015-07-21 15:19:37 +00:00
line := i + 2 + len(t.header)
2015-04-21 14:50:53 +00:00
if t.inlineInfo {
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]
}
} else if current {
label = ">"
}
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)
2016-11-15 14:57:32 +00:00
t.window.CPrint(tui.ColCursor, t.strong, label)
2015-01-01 19:49:30 +00:00
if current {
if selected {
2016-11-15 14:57:32 +00:00
t.window.CPrint(tui.ColSelected, t.strong, ">")
2015-01-01 19:49:30 +00:00
} else {
2016-11-15 14:57:32 +00:00
t.window.CPrint(tui.ColCurrent, t.strong, " ")
2015-01-01 19:49:30 +00:00
}
newLine.width = t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true)
2015-01-01 19:49:30 +00:00
} else {
if selected {
2016-11-15 14:57:32 +00:00
t.window.CPrint(tui.ColSelected, t.strong, ">")
2015-01-01 19:49:30 +00:00
} else {
t.window.Print(" ")
2015-01-01 19:49:30 +00:00
}
newLine.width = t.printHighlighted(result, 0, 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
}
2017-01-07 16:30:31 +00:00
func (t *Terminal) trimRight(runes []rune, width int) ([]rune, int) {
2015-01-18 07:59:04 +00:00
// We start from the beginning to handle tab characters
l := 0
for idx, r := range runes {
2017-01-07 16:30:31 +00:00
l += util.RuneWidth(r, l, t.tabstop)
if l > width {
2015-01-18 07:59:04 +00:00
return runes[:idx], len(runes) - idx
}
}
return runes, 0
}
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 {
2015-01-18 07:59:04 +00:00
l := 0
for _, r := range runes {
2017-01-07 16:30:31 +00:00
l += util.RuneWidth(r, l+prefixWidth, t.tabstop)
2015-01-18 07:59:04 +00:00
if l > limit {
// Early exit
return l
}
2015-01-01 19:49:30 +00:00
}
2015-01-18 07:59:04 +00:00
return l
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) {
if len(runes) > maxDisplayWidthCalc && len(runes) > width {
trimmed := len(runes) - width
return runes[trimmed:], int32(trimmed)
}
2017-01-07 16:30:31 +00:00
currentWidth := t.displayWidth(runes)
2015-01-11 18:01:24 +00:00
var trimmed int32
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, attr tui.Attr, col1 tui.ColorPair, col2 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, col2, attr, current)
2017-01-07 16:30:31 +00:00
maxWidth := t.window.Width() - 3
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
displayWidth := t.displayWidthWithLimit(text, 0, maxWidth)
if displayWidth > maxWidth {
if t.hscroll {
2015-01-01 19:49:30 +00:00
// Stri..
2017-01-07 16:30:31 +00:00
if !t.overflow(text[:maxe], maxWidth-2) {
text, _ = t.trimRight(text, maxWidth-2)
text = append(text, []rune("..")...)
} else {
// Stri..
2017-01-07 16:30:31 +00:00
if t.overflow(text[maxe:], 2) {
text = append(text[:maxe], []rune("..")...)
}
// ..ri..
var diff int32
2017-01-07 16:30:31 +00:00
text, diff = t.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...)
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("..")...)
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(col1, attr, 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, offset.attr, 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(col1, attr, substr)
}
return displayWidth
}
func numLinesMax(str string, max int) int {
lines := 0
for lines < max {
idx := strings.Index(str, "\n")
if idx < 0 {
break
}
str = str[idx+1:]
lines++
}
return lines
}
func (t *Terminal) printPreview() {
if !t.hasPreviewWindow() {
return
}
t.pwindow.Erase()
maxWidth := t.pwindow.Width()
if t.tui.DoesAutoWrap() {
maxWidth -= 1
}
reader := bufio.NewReader(strings.NewReader(t.previewer.text))
lineNo := -t.previewer.offset
height := t.pwindow.Height()
var ansi *ansiState
for {
line, err := reader.ReadString('\n')
eof := err == io.EOF
if !eof {
line = line[:len(line)-1]
}
lineNo++
if lineNo > height ||
t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
break
} else if lineNo > 0 {
var fillRet tui.FillReturn
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
trimmed := []rune(str)
if !t.preview.wrap {
trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X())
}
str, _ = t.processTabs(trimmed, 0)
if t.theme != nil && ansi != nil && ansi.colored() {
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
} else {
fillRet = t.pwindow.CFill(tui.ColNormal.Fg(), tui.ColNormal.Bg(), tui.AttrRegular, str)
}
return fillRet == tui.FillContinue
})
switch fillRet {
case tui.FillNextLine:
continue
case tui.FillSuspend:
break
2017-01-07 16:30:31 +00:00
}
t.pwindow.Fill("\n")
}
if eof {
break
}
}
2017-01-07 16:30:31 +00:00
t.pwindow.FinishFill()
if t.previewer.lines > height {
offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines)
2017-01-07 16:30:31 +00:00
pos := t.pwindow.Width() - len(offset)
if t.tui.DoesAutoWrap() {
pos -= 1
}
t.pwindow.Move(0, pos)
t.pwindow.CPrint(tui.ColInfo, tui.Reverse, offset)
}
2015-01-18 07:59:04 +00:00
}
2017-01-07 16:30:31 +00:00
func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
2015-01-18 07:59:04 +00:00
var strbuf bytes.Buffer
l := prefixWidth
for _, r := range runes {
2017-01-07 16:30:31 +00:00
w := util.RuneWidth(r, l, t.tabstop)
2015-01-18 07:59:04 +00:00
l += w
if r == '\t' {
strbuf.WriteString(strings.Repeat(" ", w))
} else {
strbuf.WriteRune(r)
}
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() {
if !t.suppress {
windows := make([]tui.Window, 0, 4)
if t.bordered {
windows = append(windows, t.border)
}
if t.hasPreviewWindow() {
windows = append(windows, t.pborder, 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
}
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 tui.Event) bool {
return event.Type == key ||
event.Type == tui.Rune && int(event.Char) == key-tui.AltZ ||
event.Type == tui.Mouse && key == tui.DoubleClick && event.MouseEvent.Double
2015-03-31 13:05:02 +00:00
}
func quoteEntryCmd(entry string) string {
escaped := strings.Replace(entry, `\`, `\\`, -1)
escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
r, _ := regexp.Compile(`[&|<>()@^%!"]`)
return r.ReplaceAllStringFunc(escaped, func(match string) string {
return "^" + match
})
}
2015-11-08 16:42:01 +00:00
func quoteEntry(entry string) string {
if util.IsWindows() {
return quoteEntryCmd(entry)
}
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
2015-11-08 16:42:01 +00:00
}
func hasPlusFlag(template string) bool {
for _, match := range placeholder.FindAllString(template, -1) {
if match[0] == '\\' {
continue
}
if match[1] == '+' {
return true
}
}
return false
}
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, 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{}
}
return placeholder.ReplaceAllStringFunc(template, func(match string) string {
// Escaped pattern
if match[0] == '\\' {
return match[1:]
}
// Current query
if match == "{q}" {
return quoteEntry(query)
}
plusFlag := forcePlus
if match[1] == '+' {
match = "{" + match[2:]
plusFlag = true
}
items := current
if plusFlag {
items = selected
}
replacements := make([]string, len(items))
if match == "{}" {
for idx, item := range items {
replacements[idx] = quoteEntry(item.AsString(stripAnsi))
}
return strings.Join(replacements, " ")
}
tokens := strings.Split(match[1:len(match)-1], ",")
ranges := make([]Range, len(tokens))
for idx, s := range tokens {
r, ok := ParseRange(&s)
if !ok {
// Invalid expression, just return the original string in the template
return match
}
ranges[idx] = r
}
for idx, item := range items {
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
trans := Transform(tokens, ranges)
str := string(joinTokens(trans))
if delimiter.str != nil {
str = strings.TrimSuffix(str, *delimiter.str)
} else if delimiter.regex != nil {
delims := delimiter.regex.FindAllStringIndex(str, -1)
if len(delims) > 0 && delims[len(delims)-1][1] == len(str) {
str = str[:delims[len(delims)-1][0]]
}
}
str = strings.TrimSpace(str)
replacements[idx] = quoteEntry(str)
}
return strings.Join(replacements, " ")
})
}
2017-04-28 13:58:08 +00:00
func (t *Terminal) redraw() {
t.tui.Clear()
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 := replacePlaceholder(template, t.ansi, t.delimiter, forcePlus, string(t.input), list)
cmd := util.ExecCommand(command)
2017-01-27 08:46:56 +00:00
if !background {
cmd.Stdin = os.Stdin
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()
2017-04-28 13:58:08 +00:00
t.tui.Resume(true)
t.redraw()
2017-01-27 08:46:56 +00:00
t.refresh()
} else {
cmd.Run()
}
}
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 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()
if !forcePlus && !hasPlusFlag(template) || len(t.selected) == 0 {
return current != nil, []*Item{current, current}
}
sels := make([]*Item, len(t.selected)+1)
sels[0] = current
for i, sel := range t.sortSelected() {
sels[i+1] = sel.item
}
return true, sels
}
2017-02-18 14:17:29 +00:00
func (t *Terminal) truncateQuery() {
maxPatternLength := util.Max(1, t.window.Width()-t.promptLen-1)
2017-02-18 14:17:29 +00:00
t.input, _ = t.trimRight(t.input, maxPatternLength)
t.cx = util.Constrain(t.cx, 0, len(t.input))
}
func (t *Terminal) selectItem(item *Item) {
t.selected[item.Index()] = selectedItem{time.Now(), item}
t.version++
}
func (t *Terminal) deselectItem(item *Item) {
delete(t.selected, item.Index())
t.version++
}
func (t *Terminal) toggleItem(item *Item) {
if _, found := t.selected[item.Index()]; !found {
t.selectItem(item)
} else {
t.deselectItem(item)
}
}
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)
2016-02-02 08:51:21 +00:00
signal.Notify(intChan, os.Interrupt, os.Kill, syscall.SIGTERM)
go func() {
<-intChan
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(reqRedraw, nil)
}
}()
t.mutex.Lock()
t.initFunc()
t.resizeWindows()
t.printPrompt()
t.placeCursor()
t.refresh()
t.printInfo()
t.printHeader()
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()
if !reading {
break
}
time.Sleep(spinnerDuration)
t.reqBox.Set(reqInfo, nil)
}
}()
2015-01-01 19:49:30 +00:00
}
if t.hasPreviewer() {
go func() {
for {
var request []*Item
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
switch req {
case reqPreviewEnqueue:
request = value.([]*Item)
}
}
events.Clear()
})
// We don't display preview window if no match
if request[0] != nil {
command := replacePlaceholder(t.preview.command,
t.ansi, t.delimiter, false, string(t.input), request)
cmd := util.ExecCommand(command)
if t.pwindow != nil {
env := os.Environ()
env = append(env, fmt.Sprintf("LINES=%d", t.pwindow.Height()))
env = append(env, fmt.Sprintf("COLUMNS=%d", t.pwindow.Width()))
cmd.Env = env
}
out, _ := cmd.CombinedOutput()
t.reqBox.Set(reqPreviewDisplay, string(out))
} else {
t.reqBox.Set(reqPreviewDisplay, "")
}
}
}()
}
exit := func(getCode func() int) {
if !t.cleanExit && t.fullscreen && t.inlineInfo {
t.placeCursor()
}
t.tui.Close()
code := getCode()
if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input))
}
2016-09-07 00:58:18 +00:00
// prof.Stop()
os.Exit(code)
}
2015-01-01 19:49:30 +00:00
go func() {
var focused *Item
var version int64
2015-01-01 19:49:30 +00:00
for {
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()
2015-04-21 14:50:53 +00:00
if t.inlineInfo {
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()
currentFocus := t.currentItem()
if currentFocus != focused || version != t.version {
version = t.version
focused = currentFocus
if t.isPreviewEnabled() {
_, list := t.buildPlusList(t.preview.command, false)
t.previewBox.Set(reqPreviewEnqueue, list)
}
}
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.redraw()
2015-01-11 18:01:24 +00:00
case reqRedraw:
2017-04-28 13:58:08 +00:00
t.redraw()
2015-01-11 18:01:24 +00:00
case reqClose:
exit(func() int {
if t.output() {
return exitOk
}
return exitNoMatch
})
case reqPreviewDisplay:
t.previewer.text = value.(string)
t.previewer.lines = strings.Count(t.previewer.text, "\n")
t.previewer.offset = 0
t.printPreview()
case reqPreviewRefresh:
t.printPreview()
case reqPrintQuery:
exit(func() int {
t.printer(string(t.input))
return exitOk
})
2015-01-11 18:01:24 +00:00
case reqQuit:
exit(func() int { return exitInterrupt })
2015-01-01 19:49:30 +00:00
}
}
t.placeCursor()
2015-01-01 19:49:30 +00:00
t.mutex.Unlock()
})
t.refresh()
}
}()
looping := true
for looping {
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
2015-01-12 03:56:17 +00:00
events := []util.EventType{reqPrompt}
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
}
}
}
toggle := func() {
if t.cy < t.merger.Length() {
t.toggleItem(t.merger.Get(t.cy).item)
2015-01-11 18:01:24 +00:00
req(reqInfo)
}
}
scrollPreview := func(amount int) {
t.previewer.offset = util.Constrain(
t.previewer.offset+amount, 0, t.previewer.lines-1)
req(reqPreviewRefresh)
}
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
var doAction func(action, int) bool
doActions := func(actions []action, mapkey int) bool {
for _, action := range actions {
if !doAction(action, mapkey) {
return false
}
}
return true
}
doAction = func(a action, mapkey int) 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() {
t.previewer.enabled = !t.previewer.enabled
t.tui.Clear()
t.resizeWindows()
if t.previewer.enabled {
valid, list := t.buildPlusList(t.preview.command, false)
if valid {
t.previewBox.Set(reqPreviewEnqueue, list)
}
}
req(reqList, reqInfo, reqHeader)
}
2017-02-18 14:49:00 +00:00
case actTogglePreviewWrap:
if t.hasPreviewWindow() {
t.preview.wrap = !t.preview.wrap
req(reqPreviewRefresh)
}
2015-10-12 17:24:38 +00:00
case actToggleSort:
t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock()
return false
case actPreviewUp:
if t.hasPreviewWindow() {
scrollPreview(-1)
}
case actPreviewDown:
if t.hasPreviewWindow() {
scrollPreview(1)
}
case actPreviewPageUp:
if t.hasPreviewWindow() {
2017-01-07 16:30:31 +00:00
scrollPreview(-t.pwindow.Height())
}
case actPreviewPageDown:
if t.hasPreviewWindow() {
2017-01-07 16:30:31 +00:00
scrollPreview(t.pwindow.Height())
}
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)
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
}
2015-10-12 17:24:38 +00:00
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++ {
t.selectItem(t.merger.Get(i).item)
2015-10-12 17:24:38 +00:00
}
req(reqList, reqInfo)
}
case actDeselectAll:
if t.multi {
2017-07-28 04:13:03 +00:00
t.selected = make(map[int32]selectedItem)
t.version++
2015-10-12 17:24:38 +00:00
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++ {
t.toggleItem(t.merger.Get(i).item)
2015-10-12 17:24:38 +00:00
}
req(reqList, reqInfo)
}
case actToggleIn:
if t.reverse {
return doAction(action{t: actToggleUp}, mapkey)
}
return doAction(action{t: actToggleDown}, mapkey)
case actToggleOut:
if t.reverse {
return doAction(action{t: actToggleDown}, mapkey)
}
return doAction(action{t: actToggleUp}, mapkey)
2015-10-12 17:24:38 +00:00
case actToggleDown:
if t.multi && t.merger.Length() > 0 {
toggle()
t.vmove(-1, true)
2015-10-12 17:24:38 +00:00
req(reqList)
}
case actToggleUp:
if t.multi && 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 actClearScreen:
req(reqRedraw)
case actTop:
t.vset(0)
req(reqList)
2015-10-12 17:24:38 +00:00
case actUnixLineDiscard:
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:
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:
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)
}
2017-04-28 13:58:08 +00:00
case actSigStop:
p, err := os.FindProcess(os.Getpid())
if err == nil {
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 {
2015-10-12 17:24:38 +00:00
if t.multi && 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) {
scrollPreview(-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
if !t.reverse {
2017-01-07 16:30:31 +00:00
my = t.window.Height() - my - 1
2015-10-12 17:24:38 +00:00
}
min := 2 + len(t.header)
if t.inlineInfo {
min--
}
if me.Double {
// Double-click
if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
return doActions(t.keymap[tui.DoubleClick], tui.DoubleClick)
2015-10-12 17:24:38 +00:00
}
}
} 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)
if me.Left {
return doActions(t.keymap[tui.LeftClick], tui.LeftClick)
}
return doActions(t.keymap[tui.RightClick], tui.RightClick)
2015-10-12 17:24:38 +00:00
}
}
2015-01-01 19:49:30 +00:00
}
}
2015-10-12 17:24:38 +00:00
return true
}
changed := false
mapkey := event.Type
if t.jumping == jumpDisabled {
actions := t.keymap[mapkey]
if mapkey == tui.Rune {
mapkey = int(event.Char) + int(tui.AltZ)
if act, prs := t.keymap[mapkey]; prs {
actions = act
}
}
if !doActions(actions, mapkey) {
continue
}
2017-02-18 14:17:29 +00:00
t.truncateQuery()
changed = string(previousInput) != string(t.input)
if onChanges, prs := t.keymap[tui.Change]; changed && prs {
if !doActions(onChanges, tui.Change) {
continue
}
}
} else {
if mapkey == 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
t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed {
2015-04-16 13:13:31 +00:00
t.eventBox.Set(EvtSearchNew, t.sort)
2015-01-01 19:49:30 +00:00
}
for _, event := range events {
t.reqBox.Set(event, nil)
}
}
}
func (t *Terminal) constrain() {
count := t.merger.Length()
2015-04-21 14:50:53 +00:00
height := t.maxItems()
2015-01-01 19:49:30 +00:00
diffpos := t.cy - t.offset
2015-01-12 03:56:17 +00:00
t.cy = util.Constrain(t.cy, 0, count-1)
t.offset = util.Constrain(t.offset, t.cy-height+1, t.cy)
2015-01-01 19:49:30 +00:00
// Adjustment
if count-t.offset < height {
2015-01-12 03:56:17 +00:00
t.offset = util.Max(0, count-height)
t.cy = util.Constrain(t.offset+diffpos, 0, count-1)
2015-01-01 19:49:30 +00:00
}
t.offset = util.Max(0, t.offset)
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) vmove(o int, allowCycle bool) {
2015-01-01 19:49:30 +00:00
if t.reverse {
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)
2015-04-21 14:50:53 +00:00
if t.inlineInfo {
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
}