fzf/src/terminal.go

1120 lines
23 KiB
Go
Raw Normal View History

2015-01-01 19:49:30 +00:00
package fzf
import (
2015-01-18 07:59:04 +00:00
"bytes"
2015-01-01 19:49:30 +00:00
"fmt"
"os"
"os/exec"
2015-01-23 11:30:50 +00:00
"os/signal"
2015-01-01 19:49:30 +00:00
"regexp"
"sort"
2015-07-26 14:02:04 +00:00
"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
C "github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util"
"github.com/junegunn/go-runewidth"
2015-01-01 19:49:30 +00:00
)
2015-01-11 18:01:24 +00:00
// Terminal represents terminal input/output
2015-01-01 19:49:30 +00:00
type Terminal struct {
2015-04-21 14:50:53 +00:00
inlineInfo bool
2015-01-01 19:49:30 +00:00
prompt string
reverse bool
hscroll bool
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
expect map[int]string
2015-05-20 12:25:15 +00:00
keymap map[int]actionType
execmap map[int]string
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
2015-07-26 14:02:04 +00:00
margin [4]string
marginInt [4]int
2015-01-01 19:49:30 +00:00
count int
progress int
reading bool
merger *Merger
selected map[uint32]selectedItem
2015-01-12 03:56:17 +00:00
reqBox *util.EventBox
eventBox *util.EventBox
2015-01-01 19:49:30 +00:00
mutex sync.Mutex
initFunc func()
suppress bool
startChan chan bool
2015-01-01 19:49:30 +00:00
}
type selectedItem struct {
at time.Time
text *string
}
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{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
var _runeWidths = make(map[rune]int)
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
reqRefresh
reqRedraw
reqClose
reqQuit
2015-01-01 19:49:30 +00:00
)
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
actDown
actUp
actPageUp
actPageDown
actToggleSort
actPreviousHistory
actNextHistory
actExecute
2015-05-20 12:25:15 +00:00
)
func defaultKeymap() map[int]actionType {
keymap := make(map[int]actionType)
keymap[C.Invalid] = actInvalid
keymap[C.CtrlA] = actBeginningOfLine
keymap[C.CtrlB] = actBackwardChar
keymap[C.CtrlC] = actAbort
keymap[C.CtrlG] = actAbort
keymap[C.CtrlQ] = actAbort
keymap[C.ESC] = actAbort
2015-08-02 04:06:15 +00:00
keymap[C.CtrlD] = actDeleteCharEOF
2015-05-20 12:25:15 +00:00
keymap[C.CtrlE] = actEndOfLine
keymap[C.CtrlF] = actForwardChar
keymap[C.CtrlH] = actBackwardDeleteChar
keymap[C.BSpace] = actBackwardDeleteChar
2015-05-20 12:25:15 +00:00
keymap[C.Tab] = actToggleDown
keymap[C.BTab] = actToggleUp
keymap[C.CtrlJ] = actDown
keymap[C.CtrlK] = actUp
keymap[C.CtrlL] = actClearScreen
keymap[C.CtrlM] = actAccept
keymap[C.CtrlN] = actDown
keymap[C.CtrlP] = actUp
keymap[C.CtrlU] = actUnixLineDiscard
keymap[C.CtrlW] = actUnixWordRubout
keymap[C.CtrlY] = actYank
keymap[C.AltB] = actBackwardWord
keymap[C.SLeft] = actBackwardWord
keymap[C.AltF] = actForwardWord
keymap[C.SRight] = actForwardWord
keymap[C.AltD] = actKillWord
keymap[C.AltBS] = actBackwardKillWord
keymap[C.Up] = actUp
keymap[C.Down] = actDown
keymap[C.Left] = actBackwardChar
keymap[C.Right] = actForwardChar
keymap[C.Home] = actBeginningOfLine
keymap[C.End] = actEndOfLine
keymap[C.Del] = actDeleteChar
2015-05-20 12:25:15 +00:00
keymap[C.PgUp] = actPageUp
keymap[C.PgDn] = actPageDown
keymap[C.Rune] = actRune
keymap[C.Mouse] = actMouse
return keymap
}
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 {
2015-01-01 19:49:30 +00:00
input := []rune(opts.Query)
var header []string
if opts.Reverse {
header = opts.Header
} else {
header = reverseStringArray(opts.Header)
}
2015-01-01 19:49:30 +00:00
return &Terminal{
2015-04-21 14:50:53 +00:00
inlineInfo: opts.InlineInfo,
2015-01-01 19:49:30 +00:00
prompt: opts.Prompt,
reverse: opts.Reverse,
hscroll: opts.Hscroll,
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,
expect: opts.Expect,
2015-05-20 12:25:15 +00:00
keymap: opts.Keymap,
execmap: opts.Execmap,
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,
marginInt: [4]int{0, 0, 0, 0},
cycle: opts.Cycle,
header: header,
header0: header,
ansi: opts.Ansi,
reading: true,
merger: EmptyMerger,
selected: make(map[uint32]selectedItem),
2015-01-12 03:56:17 +00:00
reqBox: util.NewEventBox(),
2015-01-01 19:49:30 +00:00
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
startChan: make(chan bool, 1),
2015-01-01 19:49:30 +00:00
initFunc: func() {
C.Init(opts.Theme, opts.Black, opts.Mouse)
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
2015-01-01 19:49:30 +00:00
func (t *Terminal) UpdateCount(cnt int, final bool) {
t.mutex.Lock()
t.count = cnt
t.reading = !final
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 {
fmt.Println(string(t.input))
}
if len(t.expect) > 0 {
fmt.Println(t.pressed)
}
found := len(t.selected) > 0
if !found {
cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy {
fmt.Println(t.merger.Get(t.cy).AsString(t.ansi))
found = true
2015-01-01 19:49:30 +00:00
}
} else {
sels := make([]selectedItem, 0, len(t.selected))
for _, sel := range t.selected {
sels = append(sels, sel)
}
2015-03-22 07:05:54 +00:00
sort.Sort(byTimeOrder(sels))
for _, sel := range sels {
fmt.Println(*sel.text)
2015-01-01 19:49:30 +00:00
}
}
return found
2015-01-01 19:49:30 +00:00
}
2015-01-18 07:59:04 +00:00
func runeWidth(r rune, prefixWidth int) int {
if r == '\t' {
return 8 - prefixWidth%8
} else if w, found := _runeWidths[r]; found {
return w
2015-01-18 07:59:04 +00:00
} else {
w := runewidth.RuneWidth(r)
_runeWidths[r] = w
return w
2015-01-18 07:59:04 +00:00
}
}
2015-01-01 19:49:30 +00:00
func displayWidth(runes []rune) int {
l := 0
for _, r := range runes {
2015-01-18 07:59:04 +00:00
l += runeWidth(r, l)
2015-01-01 19:49:30 +00:00
}
return l
}
2015-07-26 14:02:04 +00:00
const minWidth = 16
const minHeight = 4
func (t *Terminal) calculateMargins() {
screenWidth := C.MaxX()
screenHeight := C.MaxY()
for idx, str := range t.margin {
if str == "0" {
t.marginInt[idx] = 0
} else if strings.HasSuffix(str, "%") {
num, _ := strconv.ParseFloat(str[:len(str)-1], 64)
var val float64
if idx%2 == 0 {
val = float64(screenHeight)
} else {
val = float64(screenWidth)
}
t.marginInt[idx] = int(val * num * 0.01)
} else {
num, _ := strconv.Atoi(str)
t.marginInt[idx] = num
}
}
adjust := func(idx1 int, idx2 int, max int, min int) {
if max >= min {
margin := t.marginInt[idx1] + t.marginInt[idx2]
if max-margin < min {
desired := max - min
t.marginInt[idx1] = desired * t.marginInt[idx1] / margin
t.marginInt[idx2] = desired * t.marginInt[idx2] / margin
}
}
}
adjust(1, 3, screenWidth, minWidth)
adjust(0, 2, screenHeight, minHeight)
}
2015-01-01 19:49:30 +00:00
func (t *Terminal) move(y int, x int, clear bool) {
2015-07-26 14:02:04 +00:00
x += t.marginInt[3]
2015-01-01 19:49:30 +00:00
maxy := C.MaxY()
if !t.reverse {
2015-07-26 14:02:04 +00:00
y = maxy - y - 1 - t.marginInt[2]
} else {
y += t.marginInt[0]
2015-01-01 19:49:30 +00:00
}
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 18:01:24 +00:00
C.CPrint(C.ColPrompt, true, t.prompt)
C.CPrint(C.ColNormal, true, string(t.input))
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) printInfo() {
2015-04-21 14:50:53 +00:00
if t.inlineInfo {
t.move(0, len(t.prompt)+displayWidth(t.input)+1, true)
if t.reading {
C.CPrint(C.ColSpinner, true, " < ")
} else {
C.CPrint(C.ColPrompt, true, " < ")
}
} else {
t.move(1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
C.CPrint(C.ColSpinner, true, _spinner[idx])
}
t.move(1, 2, false)
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 += " "
}
}
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)
}
2015-01-11 18:01:24 +00:00
C.CPrint(C.ColInfo, false, output)
2015-01-01 19:49:30 +00:00
}
2015-07-26 14:02:04 +00:00
func (t *Terminal) maxHeight() int {
return C.MaxY() - t.marginInt[0] - t.marginInt[2]
}
2015-07-21 15:19:37 +00:00
func (t *Terminal) printHeader() {
if len(t.header) == 0 {
return
}
2015-07-26 14:02:04 +00:00
max := t.maxHeight()
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)
state = newState
2015-07-21 15:19:37 +00:00
item := &Item{
text: []rune(trimmed),
2015-07-21 15:19:37 +00:00
index: 0,
colors: colors,
rank: Rank{0, 0, 0}}
t.move(line, 2, true)
t.printHighlighted(item, false, C.ColHeader, 0, false)
}
}
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
2015-01-01 19:49:30 +00:00
for i := 0; i < maxy; i++ {
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
}
t.move(line, 0, true)
2015-01-01 19:49:30 +00:00
if i < count {
t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset)
2015-01-01 19:49:30 +00:00
}
}
}
func (t *Terminal) printItem(item *Item, current bool) {
_, selected := t.selected[item.index]
2015-01-01 19:49:30 +00:00
if current {
2015-01-11 18:01:24 +00:00
C.CPrint(C.ColCursor, true, ">")
2015-01-01 19:49:30 +00:00
if selected {
C.CPrint(C.ColSelected, true, ">")
2015-01-01 19:49:30 +00:00
} else {
2015-01-11 18:01:24 +00:00
C.CPrint(C.ColCurrent, true, " ")
2015-01-01 19:49:30 +00:00
}
2015-03-18 16:59:14 +00:00
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true)
2015-01-01 19:49:30 +00:00
} else {
2015-01-11 18:01:24 +00:00
C.CPrint(C.ColCursor, true, " ")
2015-01-01 19:49:30 +00:00
if selected {
2015-01-11 18:01:24 +00:00
C.CPrint(C.ColSelected, true, ">")
2015-01-01 19:49:30 +00:00
} else {
C.Print(" ")
}
2015-03-18 16:59:14 +00:00
t.printHighlighted(item, false, 0, C.ColMatch, false)
2015-01-01 19:49:30 +00:00
}
}
func 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 {
l += runeWidth(r, l)
if idx > 0 && l > width {
return runes[:idx], len(runes) - idx
}
}
return runes, 0
}
2015-01-01 19:49:30 +00:00
2015-01-18 07:59:04 +00:00
func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
l := 0
for _, r := range runes {
l += runeWidth(r, l+prefixWidth)
if l > limit {
// Early exit
return l
}
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
}
2015-01-08 17:37:08 +00:00
func trimLeft(runes []rune, width int) ([]rune, int32) {
2015-01-01 19:49:30 +00:00
currentWidth := 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++
2015-01-18 07:59:04 +00:00
currentWidth = displayWidthWithLimit(runes, 2, width)
2015-01-01 19:49:30 +00:00
}
return runes, trimmed
}
func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) {
2015-01-11 18:01:24 +00:00
var maxe int32
2015-01-01 19:49:30 +00:00
for _, offset := range item.offsets {
if offset[1] > maxe {
maxe = offset[1]
}
}
// Overflow
2015-08-02 15:14:34 +00:00
text := make([]rune, len(item.text))
copy(text, item.text)
2015-03-22 07:05:54 +00:00
offsets := item.colorOffsets(col2, bold, current)
2015-07-26 14:02:04 +00:00
maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3]
2015-01-01 19:49:30 +00:00
fullWidth := displayWidth(text)
if fullWidth > maxWidth {
if t.hscroll {
2015-01-01 19:49:30 +00:00
// Stri..
matchEndWidth := displayWidth(text[:maxe])
if matchEndWidth <= maxWidth-2 {
text, _ = trimRight(text, maxWidth-2)
text = append(text, []rune("..")...)
} else {
// Stri..
if matchEndWidth < fullWidth-2 {
text = append(text[:maxe], []rune("..")...)
}
// ..ri..
var diff int32
text, diff = trimLeft(text, maxWidth-2)
// Transform offsets
for idx, offset := range offsets {
b, e := offset.offset[0], offset.offset[1]
b += 2 - diff
e += 2 - diff
b = util.Max32(b, 2)
offsets[idx].offset[0] = b
offsets[idx].offset[1] = util.Max32(b, e)
}
text = append([]rune(".."), text...)
2015-01-01 19:49:30 +00:00
}
} else {
text, _ = 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
}
}
}
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
substr, prefixWidth = processTabs(text[index:b], prefixWidth)
C.CPrint(col1, bold, substr)
2015-03-19 03:14:26 +00:00
if b < e {
substr, prefixWidth = processTabs(text[b:e], prefixWidth)
2015-03-22 08:43:28 +00:00
C.CPrint(offset.color, offset.bold, 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 {
2015-01-18 07:59:04 +00:00
substr, _ = processTabs(text[index:], prefixWidth)
C.CPrint(col1, bold, substr)
}
}
func processTabs(runes []rune, prefixWidth int) (string, int) {
var strbuf bytes.Buffer
l := prefixWidth
for _, r := range runes {
w := runeWidth(r, l)
l += w
if r == '\t' {
strbuf.WriteString(strings.Repeat(" ", w))
} else {
strbuf.WriteRune(r)
}
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() {
2015-07-26 14:02:04 +00:00
t.calculateMargins()
2015-01-01 19:49:30 +00:00
t.printList()
t.printPrompt()
t.printInfo()
2015-07-21 15:47:14 +00:00
t.printHeader()
2015-01-01 19:49:30 +00:00
}
func (t *Terminal) refresh() {
if !t.suppress {
C.Refresh()
}
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...)
}
2015-03-31 13:05:02 +00:00
func keyMatch(key int, event C.Event) bool {
return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ
}
func executeCommand(template string, current string) {
command := strings.Replace(template, "{}", fmt.Sprintf("%q", current), -1)
cmd := exec.Command("sh", "-c", command)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
C.Endwin()
cmd.Run()
C.Refresh()
}
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() {
<-t.startChan
2015-01-01 19:49:30 +00:00
{ // Late initialization
t.mutex.Lock()
t.initFunc()
2015-07-26 14:02:04 +00:00
t.calculateMargins()
2015-01-01 19:49:30 +00:00
t.printPrompt()
t.placeCursor()
C.Refresh()
t.printInfo()
2015-07-21 15:19:37 +00:00
t.printHeader()
2015-01-01 19:49:30 +00:00
t.mutex.Unlock()
go func() {
2015-01-11 18:01:24 +00:00
timer := time.NewTimer(initialDelay)
<-timer.C
2015-01-11 18:01:24 +00:00
t.reqBox.Set(reqRefresh, nil)
}()
2015-01-23 11:30:50 +00:00
resizeChan := make(chan os.Signal, 1)
signal.Notify(resizeChan, syscall.SIGWINCH)
go func() {
for {
<-resizeChan
t.reqBox.Set(reqRedraw, nil)
}
}()
// Keep the spinner spinning
go func() {
for {
t.mutex.Lock()
reading := t.reading
t.mutex.Unlock()
if !reading {
break
}
time.Sleep(spinnerDuration)
t.reqBox.Set(reqInfo, nil)
}
}()
2015-01-01 19:49:30 +00:00
}
exit := func(code int) {
if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input))
}
os.Exit(code)
}
2015-01-01 19:49:30 +00:00
go func() {
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 := range *events {
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()
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
2015-01-11 18:01:24 +00:00
case reqRedraw:
2015-01-01 19:49:30 +00:00
C.Clear()
2015-01-23 11:30:50 +00:00
C.Endwin()
C.Refresh()
2015-01-01 19:49:30 +00:00
t.printAll()
2015-01-11 18:01:24 +00:00
case reqClose:
2015-01-01 19:49:30 +00:00
C.Close()
if t.output() {
exit(exitOk)
}
exit(exitNoMatch)
2015-01-11 18:01:24 +00:00
case reqQuit:
2015-01-01 19:49:30 +00:00
C.Close()
exit(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 {
event := C.GetChar()
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
}
}
}
selectItem := func(item *Item) bool {
if _, found := t.selected[item.index]; !found {
t.selected[item.index] = selectedItem{time.Now(), item.StringPtr(t.ansi)}
return true
}
return false
}
toggleY := func(y int) {
item := t.merger.Get(y)
if !selectItem(item) {
delete(t.selected, item.index)
}
}
toggle := func() {
if t.cy < t.merger.Length() {
toggleY(t.cy)
2015-01-11 18:01:24 +00:00
req(reqInfo)
}
}
for key, ret := range t.expect {
2015-03-31 13:05:02 +00:00
if keyMatch(key, event) {
t.pressed = ret
req(reqClose)
break
}
}
2015-05-20 12:25:15 +00:00
action := t.keymap[event.Type]
mapkey := event.Type
2015-05-20 12:25:15 +00:00
if event.Type == C.Rune {
mapkey = int(event.Char) + int(C.AltZ)
if act, prs := t.keymap[mapkey]; prs {
2015-05-20 12:25:15 +00:00
action = act
2015-03-31 13:05:02 +00:00
}
}
2015-05-20 12:25:15 +00:00
switch action {
2015-06-13 16:54:56 +00:00
case actIgnore:
case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy)
executeCommand(t.execmap[mapkey], item.AsString(t.ansi))
}
2015-05-20 12:25:15 +00:00
case actInvalid:
t.mutex.Unlock()
continue
case actToggleSort:
t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort)
2015-01-08 13:04:12 +00:00
t.mutex.Unlock()
2015-01-01 19:49:30 +00:00
continue
2015-05-20 12:25:15 +00:00
case actBeginningOfLine:
2015-01-01 19:49:30 +00:00
t.cx = 0
2015-05-20 12:25:15 +00:00
case actBackwardChar:
2015-01-01 19:49:30 +00:00
if t.cx > 0 {
2015-01-11 18:01:24 +00:00
t.cx--
2015-01-01 19:49:30 +00:00
}
2015-05-20 12:25:15 +00:00
case actAbort:
2015-01-11 18:01:24 +00:00
req(reqQuit)
2015-05-20 12:25:15 +00:00
case actDeleteChar:
t.delChar()
2015-08-02 04:06:15 +00:00
case actDeleteCharEOF:
2015-01-01 19:49:30 +00:00
if !t.delChar() && t.cx == 0 {
2015-01-11 18:01:24 +00:00
req(reqQuit)
2015-01-01 19:49:30 +00:00
}
2015-05-20 12:25:15 +00:00
case actEndOfLine:
2015-01-01 19:49:30 +00:00
t.cx = len(t.input)
2015-07-23 12:05:33 +00:00
case actCancel:
if len(t.input) == 0 {
req(reqQuit)
2015-07-23 12:05:33 +00:00
} else {
t.yanked = t.input
t.input = []rune{}
t.cx = 0
}
2015-05-20 12:25:15 +00:00
case actForwardChar:
2015-01-01 19:49:30 +00:00
if t.cx < len(t.input) {
2015-01-11 18:01:24 +00:00
t.cx++
2015-01-01 19:49:30 +00:00
}
2015-05-20 12:25:15 +00:00
case actBackwardDeleteChar:
2015-01-01 19:49:30 +00:00
if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
2015-01-11 18:01:24 +00:00
t.cx--
2015-01-01 19:49:30 +00:00
}
case actSelectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
selectItem(item)
}
req(reqList, reqInfo)
}
case actDeselectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
delete(t.selected, item.index)
}
req(reqList, reqInfo)
}
case actToggle:
if t.multi && t.merger.Length() > 0 {
toggle()
req(reqList)
}
case actToggleAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
toggleY(i)
}
req(reqList, reqInfo)
}
2015-05-20 12:25:15 +00:00
case actToggleDown:
if t.multi && t.merger.Length() > 0 {
2015-01-01 19:49:30 +00:00
toggle()
t.vmove(-1)
2015-01-11 18:01:24 +00:00
req(reqList)
2015-01-01 19:49:30 +00:00
}
2015-05-20 12:25:15 +00:00
case actToggleUp:
if t.multi && t.merger.Length() > 0 {
2015-01-01 19:49:30 +00:00
toggle()
t.vmove(1)
2015-01-11 18:01:24 +00:00
req(reqList)
2015-01-01 19:49:30 +00:00
}
2015-05-20 12:25:15 +00:00
case actDown:
2015-01-01 19:49:30 +00:00
t.vmove(-1)
2015-01-11 18:01:24 +00:00
req(reqList)
2015-05-20 12:25:15 +00:00
case actUp:
2015-01-01 19:49:30 +00:00
t.vmove(1)
2015-01-11 18:01:24 +00:00
req(reqList)
2015-05-20 12:25:15 +00:00
case actAccept:
2015-01-11 18:01:24 +00:00
req(reqClose)
2015-05-20 12:25:15 +00:00
case actClearScreen:
2015-01-11 18:01:24 +00:00
req(reqRedraw)
2015-05-20 12:25:15 +00:00
case actUnixLineDiscard:
2015-01-01 19:49:30 +00:00
if t.cx > 0 {
t.yanked = copySlice(t.input[:t.cx])
t.input = t.input[t.cx:]
t.cx = 0
}
2015-05-20 12:25:15 +00:00
case actUnixWordRubout:
2015-01-01 19:49:30 +00:00
if t.cx > 0 {
t.rubout("\\s\\S")
}
2015-05-20 12:25:15 +00:00
case actBackwardKillWord:
2015-01-01 19:49:30 +00:00
if t.cx > 0 {
t.rubout("[^[:alnum:]][[:alnum:]]")
}
2015-05-20 12:25:15 +00:00
case actYank:
suffix := copySlice(t.input[t.cx:])
t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
2015-01-01 19:49:30 +00:00
t.cx += len(t.yanked)
2015-05-20 12:25:15 +00:00
case actPageUp:
2015-04-21 14:50:53 +00:00
t.vmove(t.maxItems() - 1)
2015-01-11 18:01:24 +00:00
req(reqList)
2015-05-20 12:25:15 +00:00
case actPageDown:
2015-04-21 14:50:53 +00:00
t.vmove(-(t.maxItems() - 1))
2015-01-11 18:01:24 +00:00
req(reqList)
2015-05-20 12:25:15 +00:00
case actBackwardWord:
2015-01-01 19:49:30 +00:00
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
2015-05-20 12:25:15 +00:00
case actForwardWord:
2015-01-01 19:49:30 +00:00
t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
2015-05-20 12:25:15 +00:00
case actKillWord:
2015-01-01 19:49:30 +00: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-05-20 12:25:15 +00:00
case actKillLine:
if t.cx < len(t.input) {
t.yanked = copySlice(t.input[t.cx:])
t.input = t.input[:t.cx]
}
case actRune:
2015-01-01 19:49:30 +00:00
prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
2015-01-11 18:01:24 +00:00
t.cx++
case actPreviousHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = []rune(t.history.previous())
t.cx = len(t.input)
}
case actNextHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = []rune(t.history.next())
t.cx = len(t.input)
}
2015-05-20 12:25:15 +00:00
case actMouse:
2015-01-01 19:49:30 +00:00
me := event.MouseEvent
2015-07-26 14:02:04 +00:00
mx, my := me.X, me.Y
2015-07-26 15:06:44 +00:00
if me.S != 0 {
// Scroll
if t.merger.Length() > 0 {
if t.multi && me.Mod {
toggle()
}
t.vmove(me.S)
req(reqList)
}
} else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] &&
2015-07-26 14:02:04 +00:00
my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] {
mx -= t.marginInt[3]
my -= t.marginInt[0]
mx = util.Constrain(mx-len(t.prompt), 0, len(t.input))
if !t.reverse {
my = t.maxHeight() - my - 1
2015-01-01 19:49:30 +00:00
}
2015-07-26 14:02:04 +00:00
min := 2 + len(t.header)
if t.inlineInfo {
2015-08-02 04:06:15 +00:00
min--
2015-01-01 19:49:30 +00:00
}
2015-07-26 15:06:44 +00:00
if me.Double {
2015-07-26 14:02:04 +00:00
// Double-click
if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
req(reqClose)
}
}
} else if me.Down {
if my == 0 && mx >= 0 {
// Prompt
t.cx = mx
} else if my >= min {
// List
if t.vset(t.offset+my-min) && t.multi && me.Mod {
toggle()
}
req(reqList)
2015-01-01 19:49:30 +00:00
}
}
}
}
changed := string(previousInput) != string(t.input)
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)
2015-01-01 19:49:30 +00: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-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) {
if t.reverse {
o *= -1
}
dest := t.cy + o
if t.cycle {
max := t.merger.Length() - 1
if dest > max {
if t.cy == max {
dest = 0
}
} else if dest < 0 {
if t.cy == 0 {
dest = max
}
}
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 {
2015-07-26 14:02:04 +00:00
max := t.maxHeight() - 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
}