Add --preview and --preview-window

Close #587
This commit is contained in:
Junegunn Choi
2016-06-11 19:59:12 +09:00
parent b8737b724b
commit 2bbc12063c
4 changed files with 467 additions and 122 deletions

View File

@@ -7,7 +7,6 @@ import (
"os/signal"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"syscall"
@@ -53,8 +52,10 @@ type Terminal struct {
header []string
header0 []string
ansi bool
margin [4]string
marginInt [4]int
margin [4]sizeSpec
window *C.Window
bwindow *C.Window
pwindow *C.Window
count int
progress int
reading bool
@@ -63,6 +64,10 @@ type Terminal struct {
merger *Merger
selected map[int32]selectedItem
reqBox *util.EventBox
preview previewOpts
previewing bool
previewTxt string
previewBox *util.EventBox
eventBox *util.EventBox
mutex sync.Mutex
initFunc func()
@@ -103,6 +108,8 @@ const (
reqRedraw
reqClose
reqPrintQuery
reqPreviewEnqueue
reqPreviewDisplay
reqQuit
)
@@ -148,6 +155,7 @@ const (
actJumpAccept
actPrintQuery
actToggleSort
actTogglePreview
actPreviousHistory
actNextHistory
actExecute
@@ -220,6 +228,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
} else {
delay = initialDelay
}
var previewBox *util.EventBox
if len(opts.Preview.command) > 0 {
previewBox = util.NewEventBox()
}
return &Terminal{
initDelay: delay,
inlineInfo: opts.InlineInfo,
@@ -242,7 +254,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
printQuery: opts.PrintQuery,
history: opts.History,
margin: opts.Margin,
marginInt: [4]int{0, 0, 0, 0},
cycle: opts.Cycle,
header: header,
header0: header,
@@ -253,6 +264,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
merger: EmptyMerger,
selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(),
preview: opts.Preview,
previewing: previewBox != nil && !opts.Preview.hidden,
previewTxt: "",
previewBox: previewBox,
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
@@ -332,7 +347,7 @@ func (t *Terminal) output() bool {
if !found {
cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy {
fmt.Println(t.merger.Get(t.cy).AsString(t.ansi))
fmt.Println(t.current())
found = true
}
} else {
@@ -372,56 +387,113 @@ func displayWidth(runes []rune) int {
return l
}
const minWidth = 16
const minHeight = 4
const (
minWidth = 16
minHeight = 4
)
func (t *Terminal) calculateMargins() {
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() {
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
marginInt := [4]int{}
for idx, sizeSpec := range t.margin {
if sizeSpec.percent {
var max float64
if idx%2 == 0 {
val = float64(screenHeight)
max = float64(screenHeight)
} else {
val = float64(screenWidth)
max = float64(screenWidth)
}
t.marginInt[idx] = int(val * num * 0.01)
marginInt[idx] = int(max * sizeSpec.size * 0.01)
} else {
num, _ := strconv.Atoi(str)
t.marginInt[idx] = num
marginInt[idx] = int(sizeSpec.size)
}
}
adjust := func(idx1 int, idx2 int, max int, min int) {
if max >= min {
margin := t.marginInt[idx1] + t.marginInt[idx2]
margin := marginInt[idx1] + 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
marginInt[idx1] = desired * marginInt[idx1] / margin
marginInt[idx2] = desired * marginInt[idx2] / margin
}
}
}
adjust(1, 3, screenWidth, minWidth)
adjust(0, 2, screenHeight, minHeight)
minAreaWidth := minWidth
minAreaHeight := minHeight
if t.isPreviewEnabled() {
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.window != nil {
t.window.Close()
}
if t.bwindow != nil {
t.bwindow.Close()
t.pwindow.Close()
}
width := screenWidth - marginInt[1] - marginInt[3]
height := screenHeight - marginInt[0] - marginInt[2]
if t.isPreviewEnabled() {
createPreviewWindow := func(y int, x int, w int, h int) {
t.bwindow = C.NewWindow(y, x, w, h, true)
t.pwindow = C.NewWindow(y+1, x+2, w-4, h-2, false)
}
switch t.preview.position {
case posUp:
pheight := calculateSize(height, t.preview.size, minHeight, 3)
t.window = C.NewWindow(
marginInt[0]+pheight, marginInt[3], width, height-pheight, false)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
case posDown:
pheight := calculateSize(height, t.preview.size, minHeight, 3)
t.window = C.NewWindow(
marginInt[0], marginInt[3], width, height-pheight, false)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
case posLeft:
pwidth := calculateSize(width, t.preview.size, minWidth, 5)
t.window = C.NewWindow(
marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false)
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
case posRight:
pwidth := calculateSize(width, t.preview.size, minWidth, 5)
t.window = C.NewWindow(
marginInt[0], marginInt[3], width-pwidth, height, false)
createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
}
} else {
t.window = C.NewWindow(
marginInt[0],
marginInt[3],
width,
height, false)
}
}
func (t *Terminal) move(y int, x int, clear bool) {
x += t.marginInt[3]
maxy := C.MaxY()
if !t.reverse {
y = maxy - y - 1 - t.marginInt[2]
} else {
y += t.marginInt[0]
y = t.window.Height - y - 1
}
if clear {
C.MoveAndClear(y, x)
t.window.MoveAndClear(y, x)
} else {
C.Move(y, x)
t.window.Move(y, x)
}
}
@@ -431,24 +503,24 @@ func (t *Terminal) placeCursor() {
func (t *Terminal) printPrompt() {
t.move(0, 0, true)
C.CPrint(C.ColPrompt, true, t.prompt)
C.CPrint(C.ColNormal, true, string(t.input))
t.window.CPrint(C.ColPrompt, true, t.prompt)
t.window.CPrint(C.ColNormal, true, string(t.input))
}
func (t *Terminal) printInfo() {
if t.inlineInfo {
t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true)
if t.reading {
C.CPrint(C.ColSpinner, true, " < ")
t.window.CPrint(C.ColSpinner, true, " < ")
} else {
C.CPrint(C.ColPrompt, true, " < ")
t.window.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.window.CPrint(C.ColSpinner, true, _spinner[idx])
}
t.move(1, 2, false)
}
@@ -467,18 +539,14 @@ func (t *Terminal) printInfo() {
if t.progress > 0 && t.progress < 100 {
output += fmt.Sprintf(" (%d%%)", t.progress)
}
C.CPrint(C.ColInfo, false, output)
}
func (t *Terminal) maxHeight() int {
return C.MaxY() - t.marginInt[0] - t.marginInt[2]
t.window.CPrint(C.ColInfo, false, output)
}
func (t *Terminal) printHeader() {
if len(t.header) == 0 {
return
}
max := t.maxHeight()
max := t.window.Height
var state *ansiState
for idx, lineStr := range t.header {
line := idx + 2
@@ -529,19 +597,19 @@ func (t *Terminal) printItem(item *Item, i int, current bool) {
} else if current {
label = ">"
}
C.CPrint(C.ColCursor, true, label)
t.window.CPrint(C.ColCursor, true, label)
if current {
if selected {
C.CPrint(C.ColSelected, true, ">")
t.window.CPrint(C.ColSelected, true, ">")
} else {
C.CPrint(C.ColCurrent, true, " ")
t.window.CPrint(C.ColCurrent, true, " ")
}
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true)
} else {
if selected {
C.CPrint(C.ColSelected, true, ">")
t.window.CPrint(C.ColSelected, true, ">")
} else {
C.Print(" ")
t.window.Print(" ")
}
t.printHighlighted(item, false, 0, C.ColMatch, false)
}
@@ -593,7 +661,7 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
text := make([]rune, len(item.text))
copy(text, item.text)
offsets := item.colorOffsets(col2, bold, current)
maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3]
maxWidth := t.window.Width - 3
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
fullWidth := displayWidth(text)
if fullWidth > maxWidth {
@@ -643,11 +711,11 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
e := util.Constrain32(offset.offset[1], index, maxOffset)
substr, prefixWidth = processTabs(text[index:b], prefixWidth)
C.CPrint(col1, bold, substr)
t.window.CPrint(col1, bold, substr)
if b < e {
substr, prefixWidth = processTabs(text[b:e], prefixWidth)
C.CPrint(offset.color, offset.bold, substr)
t.window.CPrint(offset.color, offset.bold, substr)
}
index = e
@@ -657,7 +725,29 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
}
if index < maxOffset {
substr, _ = processTabs(text[index:], prefixWidth)
C.CPrint(col1, bold, substr)
t.window.CPrint(col1, bold, substr)
}
}
func (t *Terminal) printPreview() {
trimmed, ansiOffsets, _ := extractColor(t.previewTxt, nil)
var index int32
t.pwindow.Erase()
for _, o := range ansiOffsets {
b := o.offset[0]
e := o.offset[1]
if b > index {
if !t.pwindow.Fill(trimmed[index:b]) {
return
}
}
if !t.pwindow.CFill(trimmed[b:e], o.color.fg, o.color.bg, o.color.bold) {
return
}
index = e
}
if int(index) < len(trimmed) {
t.pwindow.Fill(trimmed[index:])
}
}
@@ -677,16 +767,24 @@ func processTabs(runes []rune, prefixWidth int) (string, int) {
}
func (t *Terminal) printAll() {
t.calculateMargins()
t.resizeWindows()
t.printList()
t.printPrompt()
t.printInfo()
t.printHeader()
if t.isPreviewEnabled() {
t.printPreview()
}
}
func (t *Terminal) refresh() {
if !t.suppress {
C.Refresh()
if t.isPreviewEnabled() {
t.bwindow.Refresh()
t.pwindow.Refresh()
}
t.window.Refresh()
C.DoUpdate()
}
}
@@ -746,7 +844,7 @@ func quoteEntry(entry string) string {
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
}
func executeCommand(template string, replacement string) {
func (t *Terminal) executeCommand(template string, replacement string) {
command := strings.Replace(template, "{}", replacement, -1)
cmd := util.ExecCommand(command)
cmd.Stdin = os.Stdin
@@ -754,7 +852,19 @@ func executeCommand(template string, replacement string) {
cmd.Stderr = os.Stderr
C.Endwin()
cmd.Run()
C.Refresh()
t.refresh()
}
func (t *Terminal) hasPreviewWindow() bool {
return t.previewBox != nil
}
func (t *Terminal) isPreviewEnabled() bool {
return t.previewBox != nil && t.previewing
}
func (t *Terminal) current() string {
return t.merger.Get(t.cy).AsString(t.ansi)
}
// Loop is called to start Terminal I/O
@@ -779,10 +889,10 @@ func (t *Terminal) Loop() {
t.mutex.Lock()
t.initFunc()
t.calculateMargins()
t.resizeWindows()
t.printPrompt()
t.placeCursor()
C.Refresh()
t.refresh()
t.printInfo()
t.printHeader()
t.mutex.Unlock()
@@ -807,6 +917,29 @@ func (t *Terminal) Loop() {
}()
}
if t.hasPreviewWindow() {
go func() {
for {
focused := ""
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
switch req {
case reqPreviewEnqueue:
focused = value.(string)
}
}
events.Clear()
})
if len(focused) > 0 {
command := strings.Replace(t.preview.command, "{}", quoteEntry(focused), -1)
cmd := util.ExecCommand(command)
out, _ := cmd.CombinedOutput()
t.reqBox.Set(reqPreviewDisplay, string(out))
}
}
}()
}
exit := func(code int) {
if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input))
@@ -815,11 +948,12 @@ func (t *Terminal) Loop() {
}
go func() {
focused := ""
for {
t.reqBox.Wait(func(events *util.Events) {
defer events.Clear()
t.mutex.Lock()
for req := range *events {
for req, value := range *events {
switch req {
case reqPrompt:
t.printPrompt()
@@ -830,6 +964,21 @@ func (t *Terminal) Loop() {
t.printInfo()
case reqList:
t.printList()
cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy {
currentFocus := t.current()
if currentFocus != focused {
focused = currentFocus
if t.isPreviewEnabled() {
t.previewBox.Set(reqPreviewEnqueue, focused)
}
}
} else {
if focused != "" && t.isPreviewEnabled() {
t.pwindow.Erase()
}
focused = ""
}
case reqJump:
if t.merger.Length() == 0 {
t.jumping = jumpDisabled
@@ -850,6 +999,9 @@ func (t *Terminal) Loop() {
exit(exitOk)
}
exit(exitNoMatch)
case reqPreviewDisplay:
t.previewTxt = value.(string)
t.printPreview()
case reqPrintQuery:
C.Close()
fmt.Println(string(t.input))
@@ -915,7 +1067,7 @@ func (t *Terminal) Loop() {
case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy)
executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi)))
t.executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi)))
}
case actExecuteMulti:
if len(t.selected) > 0 {
@@ -923,13 +1075,23 @@ func (t *Terminal) Loop() {
for i, sel := range t.sortSelected() {
sels[i] = quoteEntry(*sel.text)
}
executeCommand(t.execmap[mapkey], strings.Join(sels, " "))
t.executeCommand(t.execmap[mapkey], strings.Join(sels, " "))
} else {
return doAction(actExecute, mapkey)
}
case actInvalid:
t.mutex.Unlock()
return false
case actTogglePreview:
if t.hasPreviewWindow() {
t.previewing = !t.previewing
t.resizeWindows()
cnt := t.merger.Length()
if t.previewing && cnt > 0 && cnt > t.cy {
t.previewBox.Set(reqPreviewEnqueue, t.current())
}
req(reqList, reqInfo)
}
case actToggleSort:
t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort)
@@ -1097,20 +1259,19 @@ func (t *Terminal) Loop() {
mx, my := me.X, me.Y
if me.S != 0 {
// Scroll
if t.merger.Length() > 0 {
if t.window.Enclose(my, mx) && 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] &&
my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] {
mx -= t.marginInt[3]
my -= t.marginInt[0]
} else if t.window.Enclose(my, mx) {
mx -= t.window.Left
my -= t.window.Top
mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input))
if !t.reverse {
my = t.maxHeight() - my - 1
my = t.window.Height - my - 1
}
min := 2 + len(t.header)
if t.inlineInfo {
@@ -1217,7 +1378,7 @@ func (t *Terminal) vset(o int) bool {
}
func (t *Terminal) maxItems() int {
max := t.maxHeight() - 2 - len(t.header)
max := t.window.Height - 2 - len(t.header)
if t.inlineInfo {
max++
}