fzf/src/terminal.go

655 lines
13 KiB
Go
Raw Normal View History

2015-01-01 14:49:30 -05:00
package fzf
import (
"fmt"
"os"
"regexp"
"sort"
"sync"
"time"
2015-01-11 22:56:17 -05:00
C "github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util"
"github.com/junegunn/go-runewidth"
2015-01-01 14:49:30 -05:00
)
2015-01-11 13:01:24 -05:00
// Terminal represents terminal input/output
2015-01-01 14:49:30 -05:00
type Terminal struct {
prompt string
reverse bool
tac bool
cx int
cy int
offset int
yanked []rune
input []rune
multi bool
printQuery bool
count int
progress int
reading bool
merger *Merger
selected map[*string]selectedItem
2015-01-11 22:56:17 -05:00
reqBox *util.EventBox
eventBox *util.EventBox
2015-01-01 14:49:30 -05:00
mutex sync.Mutex
initFunc func()
suppress bool
2015-01-01 14:49:30 -05:00
}
type selectedItem struct {
at time.Time
text *string
}
type ByTimeOrder []selectedItem
func (a ByTimeOrder) Len() int {
return len(a)
}
func (a ByTimeOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByTimeOrder) Less(i, j int) bool {
return a[i].at.Before(a[j].at)
}
2015-01-11 13:01:24 -05:00
var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
2015-01-01 14:49:30 -05:00
const (
2015-01-11 22:56:17 -05:00
reqPrompt util.EventType = iota
2015-01-11 13:01:24 -05:00
reqInfo
reqList
reqRefresh
reqRedraw
reqClose
reqQuit
2015-01-01 14:49:30 -05:00
)
const (
2015-01-11 13:01:24 -05:00
initialDelay = 100 * time.Millisecond
spinnerDuration = 200 * time.Millisecond
)
2015-01-11 13:01:24 -05:00
// NewTerminal returns new Terminal object
2015-01-11 22:56:17 -05:00
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
2015-01-01 14:49:30 -05:00
input := []rune(opts.Query)
return &Terminal{
prompt: opts.Prompt,
tac: opts.Sort == 0,
reverse: opts.Reverse,
cx: displayWidth(input),
cy: 0,
offset: 0,
yanked: []rune{},
input: input,
multi: opts.Multi,
printQuery: opts.PrintQuery,
merger: EmptyMerger,
selected: make(map[*string]selectedItem),
2015-01-11 22:56:17 -05:00
reqBox: util.NewEventBox(),
2015-01-01 14:49:30 -05:00
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
2015-01-01 14:49:30 -05:00
initFunc: func() {
C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse)
}}
}
2015-01-11 13:01:24 -05:00
// Input returns current query string
2015-01-01 14:49:30 -05:00
func (t *Terminal) Input() []rune {
t.mutex.Lock()
defer t.mutex.Unlock()
return copySlice(t.input)
}
2015-01-11 13:01:24 -05:00
// UpdateCount updates the count information
2015-01-01 14:49:30 -05:00
func (t *Terminal) UpdateCount(cnt int, final bool) {
t.mutex.Lock()
t.count = cnt
t.reading = !final
t.mutex.Unlock()
2015-01-11 13:01:24 -05:00
t.reqBox.Set(reqInfo, nil)
if final {
2015-01-11 13:01:24 -05:00
t.reqBox.Set(reqRefresh, nil)
}
2015-01-01 14:49:30 -05:00
}
2015-01-11 13:01:24 -05:00
// UpdateProgress updates the search progress
2015-01-01 14:49:30 -05:00
func (t *Terminal) UpdateProgress(progress float32) {
t.mutex.Lock()
newProgress := int(progress * 100)
changed := t.progress != newProgress
t.progress = newProgress
2015-01-01 14:49:30 -05:00
t.mutex.Unlock()
if changed {
2015-01-11 13:01:24 -05:00
t.reqBox.Set(reqInfo, nil)
}
2015-01-01 14:49:30 -05:00
}
2015-01-11 13:01:24 -05:00
// UpdateList updates Merger to display the list
func (t *Terminal) UpdateList(merger *Merger) {
2015-01-01 14:49:30 -05:00
t.mutex.Lock()
t.progress = 100
t.merger = merger
2015-01-01 14:49:30 -05:00
t.mutex.Unlock()
2015-01-11 13:01:24 -05:00
t.reqBox.Set(reqInfo, nil)
t.reqBox.Set(reqList, nil)
2015-01-01 14:49:30 -05:00
}
func (t *Terminal) listIndex(y int) int {
if t.tac {
return t.merger.Length() - y - 1
2015-01-01 14:49:30 -05:00
}
2015-01-11 13:01:24 -05:00
return y
2015-01-01 14:49:30 -05:00
}
func (t *Terminal) output() {
if t.printQuery {
fmt.Println(string(t.input))
}
if len(t.selected) == 0 {
cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy {
2015-01-11 13:01:24 -05:00
fmt.Println(t.merger.Get(t.listIndex(t.cy)).AsString())
2015-01-01 14:49:30 -05:00
}
} else {
sels := make([]selectedItem, 0, len(t.selected))
for _, sel := range t.selected {
sels = append(sels, sel)
}
sort.Sort(ByTimeOrder(sels))
for _, sel := range sels {
fmt.Println(*sel.text)
2015-01-01 14:49:30 -05:00
}
}
}
func displayWidth(runes []rune) int {
l := 0
for _, r := range runes {
l += runewidth.RuneWidth(r)
}
return l
}
func (t *Terminal) move(y int, x int, clear bool) {
maxy := C.MaxY()
if !t.reverse {
y = maxy - y - 1
}
if clear {
C.MoveAndClear(y, x)
} else {
C.Move(y, x)
}
}
func (t *Terminal) placeCursor() {
t.move(0, len(t.prompt)+displayWidth(t.input[:t.cx]), false)
}
func (t *Terminal) printPrompt() {
t.move(0, 0, true)
2015-01-11 13:01:24 -05:00
C.CPrint(C.ColPrompt, true, t.prompt)
C.CPrint(C.ColNormal, true, string(t.input))
2015-01-01 14:49:30 -05:00
}
func (t *Terminal) printInfo() {
t.move(1, 0, true)
if t.reading {
2015-01-11 13:01:24 -05:00
duration := int64(spinnerDuration)
2015-01-01 14:49:30 -05:00
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
2015-01-11 13:01:24 -05:00
C.CPrint(C.ColSpinner, true, _spinner[idx])
2015-01-01 14:49:30 -05:00
}
t.move(1, 2, false)
output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count)
2015-01-01 14:49:30 -05: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)
}
2015-01-11 13:01:24 -05:00
C.CPrint(C.ColInfo, false, output)
2015-01-01 14:49:30 -05:00
}
func (t *Terminal) printList() {
t.constrain()
maxy := maxItems()
count := t.merger.Length() - t.offset
2015-01-01 14:49:30 -05:00
for i := 0; i < maxy; i++ {
t.move(i+2, 0, true)
if i < count {
t.printItem(t.merger.Get(t.listIndex(i+t.offset)), i == t.cy-t.offset)
2015-01-01 14:49:30 -05:00
}
}
}
func (t *Terminal) printItem(item *Item, current bool) {
_, selected := t.selected[item.text]
if current {
2015-01-11 13:01:24 -05:00
C.CPrint(C.ColCursor, true, ">")
2015-01-01 14:49:30 -05:00
if selected {
2015-01-11 13:01:24 -05:00
C.CPrint(C.ColCurrent, true, ">")
2015-01-01 14:49:30 -05:00
} else {
2015-01-11 13:01:24 -05:00
C.CPrint(C.ColCurrent, true, " ")
2015-01-01 14:49:30 -05:00
}
2015-01-11 13:01:24 -05:00
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch)
2015-01-01 14:49:30 -05:00
} else {
2015-01-11 13:01:24 -05:00
C.CPrint(C.ColCursor, true, " ")
2015-01-01 14:49:30 -05:00
if selected {
2015-01-11 13:01:24 -05:00
C.CPrint(C.ColSelected, true, ">")
2015-01-01 14:49:30 -05:00
} else {
C.Print(" ")
}
2015-01-11 13:01:24 -05:00
t.printHighlighted(item, false, 0, C.ColMatch)
2015-01-01 14:49:30 -05:00
}
}
func trimRight(runes []rune, width int) ([]rune, int) {
currentWidth := displayWidth(runes)
trimmed := 0
for currentWidth > width && len(runes) > 0 {
sz := len(runes)
currentWidth -= runewidth.RuneWidth(runes[sz-1])
runes = runes[:sz-1]
2015-01-11 13:01:24 -05:00
trimmed++
2015-01-01 14:49:30 -05:00
}
return runes, trimmed
}
2015-01-08 12:37:08 -05:00
func trimLeft(runes []rune, width int) ([]rune, int32) {
2015-01-01 14:49:30 -05:00
currentWidth := displayWidth(runes)
2015-01-11 13:01:24 -05:00
var trimmed int32
2015-01-01 14:49:30 -05:00
for currentWidth > width && len(runes) > 0 {
currentWidth -= runewidth.RuneWidth(runes[0])
runes = runes[1:]
2015-01-11 13:01:24 -05:00
trimmed++
2015-01-01 14:49:30 -05:00
}
return runes, trimmed
}
func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) {
2015-01-11 13:01:24 -05:00
var maxe int32
2015-01-01 14:49:30 -05:00
for _, offset := range item.offsets {
if offset[1] > maxe {
maxe = offset[1]
}
}
// Overflow
text := []rune(*item.text)
offsets := item.offsets
maxWidth := C.MaxX() - 3
fullWidth := displayWidth(text)
if fullWidth > maxWidth {
// Stri..
matchEndWidth := displayWidth(text[:maxe])
if matchEndWidth <= maxWidth-2 {
text, _ = trimRight(text, maxWidth-2)
text = append(text, []rune("..")...)
} else {
// Stri..
if matchEndWidth < fullWidth-2 {
text = append(text[:maxe], []rune("..")...)
}
// ..ri..
2015-01-08 12:37:08 -05:00
var diff int32
2015-01-01 14:49:30 -05:00
text, diff = trimLeft(text, maxWidth-2)
// Transform offsets
offsets = make([]Offset, len(item.offsets))
for idx, offset := range item.offsets {
b, e := offset[0], offset[1]
b += 2 - diff
e += 2 - diff
2015-01-11 22:56:17 -05:00
b = util.Max32(b, 2)
2015-01-01 14:49:30 -05:00
if b < e {
offsets[idx] = Offset{b, e}
}
}
text = append([]rune(".."), text...)
}
}
sort.Sort(ByOrder(offsets))
2015-01-11 13:01:24 -05:00
var index int32
2015-01-01 14:49:30 -05:00
for _, offset := range offsets {
2015-01-11 22:56:17 -05:00
b := util.Max32(index, offset[0])
e := util.Max32(index, offset[1])
2015-01-01 14:49:30 -05:00
C.CPrint(col1, bold, string(text[index:b]))
C.CPrint(col2, bold, string(text[b:e]))
index = e
}
2015-01-08 12:37:08 -05:00
if index < int32(len(text)) {
2015-01-01 14:49:30 -05:00
C.CPrint(col1, bold, string(text[index:]))
}
}
func (t *Terminal) printAll() {
t.printList()
t.printInfo()
t.printPrompt()
}
func (t *Terminal) refresh() {
if !t.suppress {
C.Refresh()
}
2015-01-01 14:49:30 -05: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...)
}
2015-01-11 13:01:24 -05:00
// Loop is called to start Terminal I/O
2015-01-01 14:49:30 -05:00
func (t *Terminal) Loop() {
{ // Late initialization
t.mutex.Lock()
t.initFunc()
t.printPrompt()
t.placeCursor()
C.Refresh()
t.printInfo()
2015-01-01 14:49:30 -05:00
t.mutex.Unlock()
go func() {
2015-01-11 13:01:24 -05:00
timer := time.NewTimer(initialDelay)
<-timer.C
2015-01-11 13:01:24 -05:00
t.reqBox.Set(reqRefresh, nil)
}()
2015-01-01 14:49:30 -05:00
}
go func() {
for {
2015-01-11 22:56:17 -05:00
t.reqBox.Wait(func(events *util.Events) {
2015-01-01 14:49:30 -05:00
defer events.Clear()
t.mutex.Lock()
for req := range *events {
switch req {
2015-01-11 13:01:24 -05:00
case reqPrompt:
2015-01-01 14:49:30 -05:00
t.printPrompt()
2015-01-11 13:01:24 -05:00
case reqInfo:
2015-01-01 14:49:30 -05:00
t.printInfo()
2015-01-11 13:01:24 -05:00
case reqList:
2015-01-01 14:49:30 -05:00
t.printList()
2015-01-11 13:01:24 -05:00
case reqRefresh:
t.suppress = false
2015-01-11 13:01:24 -05:00
case reqRedraw:
2015-01-01 14:49:30 -05:00
C.Clear()
t.printAll()
2015-01-11 13:01:24 -05:00
case reqClose:
2015-01-01 14:49:30 -05:00
C.Close()
t.output()
os.Exit(0)
2015-01-11 13:01:24 -05:00
case reqQuit:
2015-01-01 14:49:30 -05:00
C.Close()
os.Exit(1)
}
}
t.placeCursor()
2015-01-01 14:49:30 -05:00
t.mutex.Unlock()
})
t.refresh()
}
}()
looping := true
for looping {
event := C.GetChar()
t.mutex.Lock()
previousInput := t.input
2015-01-11 22:56:17 -05:00
events := []util.EventType{reqPrompt}
req := func(evts ...util.EventType) {
2015-01-01 14:49:30 -05:00
for _, event := range evts {
events = append(events, event)
2015-01-11 13:01:24 -05:00
if event == reqClose || event == reqQuit {
2015-01-01 14:49:30 -05:00
looping = false
}
}
}
toggle := func() {
idx := t.listIndex(t.cy)
if idx < t.merger.Length() {
item := t.merger.Get(idx)
if _, found := t.selected[item.text]; !found {
var strptr *string
if item.origText != nil {
strptr = item.origText
} else {
strptr = item.text
}
t.selected[item.text] = selectedItem{time.Now(), strptr}
} else {
delete(t.selected, item.text)
}
2015-01-11 13:01:24 -05:00
req(reqInfo)
}
}
2015-01-01 14:49:30 -05:00
switch event.Type {
2015-01-11 13:01:24 -05:00
case C.Invalid:
2015-01-08 08:04:12 -05:00
t.mutex.Unlock()
2015-01-01 14:49:30 -05:00
continue
2015-01-11 13:01:24 -05:00
case C.CtrlA:
2015-01-01 14:49:30 -05:00
t.cx = 0
2015-01-11 13:01:24 -05:00
case C.CtrlB:
2015-01-01 14:49:30 -05:00
if t.cx > 0 {
2015-01-11 13:01:24 -05:00
t.cx--
2015-01-01 14:49:30 -05:00
}
2015-01-11 13:01:24 -05:00
case C.CtrlC, C.CtrlG, C.CtrlQ, C.ESC:
req(reqQuit)
case C.CtrlD:
2015-01-01 14:49:30 -05:00
if !t.delChar() && t.cx == 0 {
2015-01-11 13:01:24 -05:00
req(reqQuit)
2015-01-01 14:49:30 -05:00
}
2015-01-11 13:01:24 -05:00
case C.CtrlE:
2015-01-01 14:49:30 -05:00
t.cx = len(t.input)
2015-01-11 13:01:24 -05:00
case C.CtrlF:
2015-01-01 14:49:30 -05:00
if t.cx < len(t.input) {
2015-01-11 13:01:24 -05:00
t.cx++
2015-01-01 14:49:30 -05:00
}
2015-01-11 13:01:24 -05:00
case C.CtrlH:
2015-01-01 14:49:30 -05:00
if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
2015-01-11 13:01:24 -05:00
t.cx--
2015-01-01 14:49:30 -05:00
}
2015-01-11 13:01:24 -05:00
case C.Tab:
if t.multi && t.merger.Length() > 0 {
2015-01-01 14:49:30 -05:00
toggle()
t.vmove(-1)
2015-01-11 13:01:24 -05:00
req(reqList)
2015-01-01 14:49:30 -05:00
}
2015-01-11 13:01:24 -05:00
case C.BTab:
if t.multi && t.merger.Length() > 0 {
2015-01-01 14:49:30 -05:00
toggle()
t.vmove(1)
2015-01-11 13:01:24 -05:00
req(reqList)
2015-01-01 14:49:30 -05:00
}
2015-01-11 13:01:24 -05:00
case C.CtrlJ, C.CtrlN:
2015-01-01 14:49:30 -05:00
t.vmove(-1)
2015-01-11 13:01:24 -05:00
req(reqList)
case C.CtrlK, C.CtrlP:
2015-01-01 14:49:30 -05:00
t.vmove(1)
2015-01-11 13:01:24 -05:00
req(reqList)
case C.CtrlM:
req(reqClose)
case C.CtrlL:
req(reqRedraw)
case C.CtrlU:
2015-01-01 14:49:30 -05:00
if t.cx > 0 {
t.yanked = copySlice(t.input[:t.cx])
t.input = t.input[t.cx:]
t.cx = 0
}
2015-01-11 13:01:24 -05:00
case C.CtrlW:
2015-01-01 14:49:30 -05:00
if t.cx > 0 {
t.rubout("\\s\\S")
}
2015-01-11 13:01:24 -05:00
case C.AltBS:
2015-01-01 14:49:30 -05:00
if t.cx > 0 {
t.rubout("[^[:alnum:]][[:alnum:]]")
}
2015-01-11 13:01:24 -05:00
case C.CtrlY:
suffix := copySlice(t.input[t.cx:])
t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
2015-01-01 14:49:30 -05:00
t.cx += len(t.yanked)
2015-01-11 13:01:24 -05:00
case C.Del:
2015-01-01 14:49:30 -05:00
t.delChar()
2015-01-11 13:01:24 -05:00
case C.PgUp:
2015-01-01 14:49:30 -05:00
t.vmove(maxItems() - 1)
2015-01-11 13:01:24 -05:00
req(reqList)
case C.PgDn:
2015-01-01 14:49:30 -05:00
t.vmove(-(maxItems() - 1))
2015-01-11 13:01:24 -05:00
req(reqList)
case C.AltB:
2015-01-01 14:49:30 -05:00
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
2015-01-11 13:01:24 -05:00
case C.AltF:
2015-01-01 14:49:30 -05:00
t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
2015-01-11 13:01:24 -05:00
case C.AltD:
2015-01-01 14:49:30 -05:00
ncx := t.cx +
findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
if ncx > t.cx {
t.yanked = copySlice(t.input[t.cx:ncx])
t.input = append(t.input[:t.cx], t.input[ncx:]...)
}
2015-01-11 13:01:24 -05:00
case C.Rune:
2015-01-01 14:49:30 -05:00
prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
2015-01-11 13:01:24 -05:00
t.cx++
case C.Mouse:
2015-01-01 14:49:30 -05:00
me := event.MouseEvent
2015-01-11 22:56:17 -05:00
mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y
2015-01-01 14:49:30 -05:00
if !t.reverse {
my = C.MaxY() - my - 1
}
if me.S != 0 {
// Scroll
if t.merger.Length() > 0 {
if t.multi && me.Mod {
toggle()
}
t.vmove(me.S)
2015-01-11 13:01:24 -05:00
req(reqList)
2015-01-01 14:49:30 -05:00
}
} else if me.Double {
// Double-click
if my >= 2 {
2015-01-10 00:50:24 -05:00
if t.vset(my-2) && t.listIndex(t.cy) < t.merger.Length() {
2015-01-11 13:01:24 -05:00
req(reqClose)
}
2015-01-01 14:49:30 -05:00
}
} else if me.Down {
if my == 0 && mx >= 0 {
// Prompt
t.cx = mx
} else if my >= 2 {
// List
2015-01-10 00:50:24 -05:00
if t.vset(t.offset+my-2) && t.multi && me.Mod {
2015-01-01 14:49:30 -05:00
toggle()
}
2015-01-11 13:01:24 -05:00
req(reqList)
2015-01-01 14:49:30 -05:00
}
}
}
changed := string(previousInput) != string(t.input)
t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed {
2015-01-11 13:01:24 -05:00
t.eventBox.Set(EvtSearchNew, nil)
2015-01-01 14:49:30 -05:00
}
for _, event := range events {
t.reqBox.Set(event, nil)
}
}
}
func (t *Terminal) constrain() {
count := t.merger.Length()
2015-01-01 14:49:30 -05:00
height := C.MaxY() - 2
diffpos := t.cy - t.offset
2015-01-11 22:56:17 -05:00
t.cy = util.Constrain(t.cy, 0, count-1)
2015-01-01 14:49:30 -05:00
if t.cy > t.offset+(height-1) {
// Ceil
t.offset = t.cy - (height - 1)
} else if t.offset > t.cy {
// Floor
t.offset = t.cy
}
// Adjustment
if count-t.offset < height {
2015-01-11 22:56:17 -05:00
t.offset = util.Max(0, count-height)
t.cy = util.Constrain(t.offset+diffpos, 0, count-1)
2015-01-01 14:49:30 -05:00
}
}
func (t *Terminal) vmove(o int) {
if t.reverse {
2015-01-10 00:50:24 -05:00
t.vset(t.cy - o)
2015-01-01 14:49:30 -05:00
} else {
2015-01-10 00:50:24 -05:00
t.vset(t.cy + o)
2015-01-01 14:49:30 -05:00
}
2015-01-10 00:50:24 -05:00
}
func (t *Terminal) vset(o int) bool {
2015-01-11 22:56:17 -05:00
t.cy = util.Constrain(o, 0, t.merger.Length()-1)
2015-01-10 00:50:24 -05:00
return t.cy == o
2015-01-01 14:49:30 -05:00
}
func maxItems() int {
return C.MaxY() - 2
}