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
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
4 changed files with 467 additions and 122 deletions

View File

@ -113,7 +113,8 @@ const (
ColCursor ColCursor
ColSelected ColSelected
ColHeader ColHeader
ColUser ColBorder
ColUser // Should be the last entry
) )
const ( const (
@ -136,6 +137,7 @@ type ColorTheme struct {
Cursor int16 Cursor int16
Selected int16 Selected int16
Header int16 Header int16
Border int16
} }
type Event struct { type Event struct {
@ -170,6 +172,31 @@ var (
DarkBG int DarkBG int
) )
type Window struct {
win *C.WINDOW
Top int
Left int
Width int
Height int
}
func NewWindow(top int, left int, width int, height int, border bool) *Window {
win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
if border {
attr := _color(ColBorder, false)
C.wattron(win, attr)
C.box(win, 0, 0)
C.wattroff(win, attr)
}
return &Window{
win: win,
Top: top,
Left: left,
Width: width,
Height: height,
}
}
func EmptyTheme() *ColorTheme { func EmptyTheme() *ColorTheme {
return &ColorTheme{ return &ColorTheme{
UseDefault: true, UseDefault: true,
@ -184,7 +211,8 @@ func EmptyTheme() *ColorTheme {
Info: colUndefined, Info: colUndefined,
Cursor: colUndefined, Cursor: colUndefined,
Selected: colUndefined, Selected: colUndefined,
Header: colUndefined} Header: colUndefined,
Border: colUndefined}
} }
func init() { func init() {
@ -204,7 +232,8 @@ func init() {
Info: C.COLOR_WHITE, Info: C.COLOR_WHITE,
Cursor: C.COLOR_RED, Cursor: C.COLOR_RED,
Selected: C.COLOR_MAGENTA, Selected: C.COLOR_MAGENTA,
Header: C.COLOR_CYAN} Header: C.COLOR_CYAN,
Border: C.COLOR_BLACK}
Dark256 = &ColorTheme{ Dark256 = &ColorTheme{
UseDefault: true, UseDefault: true,
Fg: 15, Fg: 15,
@ -218,7 +247,8 @@ func init() {
Info: 144, Info: 144,
Cursor: 161, Cursor: 161,
Selected: 168, Selected: 168,
Header: 109} Header: 109,
Border: 59}
Light256 = &ColorTheme{ Light256 = &ColorTheme{
UseDefault: true, UseDefault: true,
Fg: 15, Fg: 15,
@ -232,7 +262,8 @@ func init() {
Info: 101, Info: 101,
Cursor: 161, Cursor: 161,
Selected: 168, Selected: 168,
Header: 31} Header: 31,
Border: 145}
} }
func attrColored(pair int, bold bool) C.int { func attrColored(pair int, bold bool) C.int {
@ -360,6 +391,7 @@ func initPairs(baseTheme *ColorTheme, theme *ColorTheme, black bool) {
C.init_pair(ColCursor, override(baseTheme.Cursor, theme.Cursor), darkBG) C.init_pair(ColCursor, override(baseTheme.Cursor, theme.Cursor), darkBG)
C.init_pair(ColSelected, override(baseTheme.Selected, theme.Selected), darkBG) C.init_pair(ColSelected, override(baseTheme.Selected, theme.Selected), darkBG)
C.init_pair(ColHeader, override(baseTheme.Header, theme.Header), bg) C.init_pair(ColHeader, override(baseTheme.Header, theme.Header), bg)
C.init_pair(ColBorder, override(baseTheme.Border, theme.Border), bg)
} }
func Close() { func Close() {
@ -415,7 +447,9 @@ func mouseSequence(sz *int) Event {
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
mod := _buf[3] >= 100 mod := _buf[3] >= 100
s := 1 - int(_buf[3]%2)*2 s := 1 - int(_buf[3]%2)*2
return Event{Mouse, 0, &MouseEvent{0, 0, s, false, false, mod}} x := int(_buf[4] - 33)
y := int(_buf[5] - 33)
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}}
} }
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
@ -588,17 +622,25 @@ func GetChar() Event {
return Event{Rune, r, nil} return Event{Rune, r, nil}
} }
func Move(y int, x int) { func (w *Window) Close() {
C.move(C.int(y), C.int(x)) C.delwin(w.win)
} }
func MoveAndClear(y int, x int) { func (w *Window) Enclose(y int, x int) bool {
Move(y, x) return bool(C.wenclose(w.win, C.int(y), C.int(x)))
C.clrtoeol()
} }
func Print(text string) { func (w *Window) Move(y int, x int) {
C.addstr(C.CString(strings.Map(func(r rune) rune { C.wmove(w.win, C.int(y), C.int(x))
}
func (w *Window) MoveAndClear(y int, x int) {
w.Move(y, x)
C.wclrtoeol(w.win)
}
func (w *Window) Print(text string) {
C.waddstr(w.win, C.CString(strings.Map(func(r rune) rune {
if r < 32 { if r < 32 {
return -1 return -1
} }
@ -606,11 +648,11 @@ func Print(text string) {
}, text))) }, text)))
} }
func CPrint(pair int, bold bool, text string) { func (w *Window) CPrint(pair int, bold bool, text string) {
attr := _color(pair, bold) attr := _color(pair, bold)
C.attron(attr) C.wattron(w.win, attr)
Print(text) w.Print(text)
C.attroff(attr) C.wattroff(w.win, attr)
} }
func Clear() { func Clear() {
@ -625,6 +667,30 @@ func Refresh() {
C.refresh() C.refresh()
} }
func (w *Window) Erase() {
C.werase(w.win)
}
func (w *Window) Fill(str string) bool {
return C.waddstr(w.win, C.CString(str)) == C.OK
}
func (w *Window) CFill(str string, fg int, bg int, bold bool) bool {
attr := _color(PairFor(fg, bg), bold)
C.wattron(w.win, attr)
ret := w.Fill(str)
C.wattroff(w.win, attr)
return ret
}
func (w *Window) Refresh() {
C.wnoutrefresh(w.win)
}
func DoUpdate() {
C.doupdate()
}
func PairFor(fg int, bg int) int { func PairFor(fg int, bg int) int {
key := (fg << 8) + bg key := (fg << 8) + bg
if found, prs := _colorMap[key]; prs { if found, prs := _colorMap[key]; prs {

View File

@ -1,6 +1,7 @@
package fzf package fzf
import ( import (
"fmt"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
@ -23,36 +24,47 @@ const usage = `usage: fzf [options]
-n, --nth=N[,..] Comma-separated list of field index expressions -n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END]). integer or a range expression ([BEGIN]..[END]).
--with-nth=N[,..] Transform item using index expressions within finder --with-nth=N[,..] Transform the presentation of each line using
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) field index expressions
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
+s, --no-sort Do not sort the result +s, --no-sort Do not sort the result
--tac Reverse the order of the input --tac Reverse the order of the input
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
when the scores are tied; when the scores are tied [length|begin|end|index]
[length|begin|end|index] (default: length) (default: length)
Interface Interface
-m, --multi Enable multi-select with tab/shift-tab -m, --multi Enable multi-select with tab/shift-tab
--ansi Enable processing of ANSI color codes
--no-mouse Disable mouse --no-mouse Disable mouse
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors --bind=KEYBINDS Custom key bindings. Refer to the man page.
--black Use black background
--reverse Reverse orientation
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--tabstop=SPACES Number of spaces for a tab character (default: 8)
--cycle Enable cyclic scroll --cycle Enable cyclic scroll
--no-hscroll Disable horizontal scroll --no-hscroll Disable horizontal scroll
--hscroll-off=COL Number of screen columns to keep to the right of the --hscroll-off=COL Number of screen columns to keep to the right of the
highlighted substring (default: 10) highlighted substring (default: 10)
--inline-info Display finder info inline with the query
--jump-labels=CHARS Label characters for jump and jump-accept --jump-labels=CHARS Label characters for jump and jump-accept
Layout
--reverse Reverse orientation
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--inline-info Display finder info inline with the query
--prompt=STR Input prompt (default: '> ') --prompt=STR Input prompt (default: '> ')
--bind=KEYBINDS Custom key bindings. Refer to the man page.
--history=FILE History file
--history-size=N Maximum number of history entries (default: 1000)
--header=STR String to print as header --header=STR String to print as header
--header-lines=N The first N lines of the input are treated as header --header-lines=N The first N lines of the input are treated as header
Display
--ansi Enable processing of ANSI color codes
--tabstop=SPACES Number of spaces for a tab character (default: 8)
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
History
--history=FILE History file
--history-size=N Maximum number of history entries (default: 1000)
Preview
--preview=COMMAND Command to preview highlighted line ({})
--preview-window=OPT Preview window layout (default: right:50%)
[up|down|left|right][:SIZE[%]][:hidden]
Scripting Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
-1, --select-1 Automatically select the only match -1, --select-1 Automatically select the only match
@ -88,8 +100,29 @@ const (
byEnd byEnd
) )
func defaultMargin() [4]string { type sizeSpec struct {
return [4]string{"0", "0", "0", "0"} size float64
percent bool
}
func defaultMargin() [4]sizeSpec {
return [4]sizeSpec{}
}
type windowPosition int
const (
posUp windowPosition = iota
posDown
posLeft
posRight
)
type previewOpts struct {
command string
position windowPosition
size sizeSpec
hidden bool
} }
// Options stores the values of command-line options // Options stores the values of command-line options
@ -123,13 +156,14 @@ type Options struct {
Expect map[int]string Expect map[int]string
Keymap map[int]actionType Keymap map[int]actionType
Execmap map[int]string Execmap map[int]string
Preview previewOpts
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
Sync bool Sync bool
History *History History *History
Header []string Header []string
HeaderLines int HeaderLines int
Margin [4]string Margin [4]sizeSpec
Tabstop int Tabstop int
Version bool Version bool
} }
@ -165,6 +199,7 @@ func defaultOptions() *Options {
Expect: make(map[int]string), Expect: make(map[int]string),
Keymap: make(map[int]actionType), Keymap: make(map[int]actionType),
Execmap: make(map[int]string), Execmap: make(map[int]string),
Preview: previewOpts{"", posRight, sizeSpec{50, true}, false},
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
Sync: false, Sync: false,
@ -458,6 +493,8 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme
theme.Match = ansi theme.Match = ansi
case "hl+": case "hl+":
theme.CurrentMatch = ansi theme.CurrentMatch = ansi
case "border":
theme.Border = ansi
case "prompt": case "prompt":
theme.Prompt = ansi theme.Prompt = ansi
case "spinner": case "spinner":
@ -604,6 +641,8 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
keymap[key] = actPreviousHistory keymap[key] = actPreviousHistory
case "next-history": case "next-history":
keymap[key] = actNextHistory keymap[key] = actNextHistory
case "toggle-preview":
keymap[key] = actTogglePreview
case "toggle-sort": case "toggle-sort":
keymap[key] = actToggleSort keymap[key] = actToggleSort
default: default:
@ -659,40 +698,86 @@ func strLines(str string) []string {
return strings.Split(strings.TrimSuffix(str, "\n"), "\n") return strings.Split(strings.TrimSuffix(str, "\n"), "\n")
} }
func parseMargin(margin string) [4]string { func parseSize(str string, maxPercent float64, label string) sizeSpec {
margins := strings.Split(margin, ",") var val float64
checked := func(str string) string { percent := strings.HasSuffix(str, "%")
if strings.HasSuffix(str, "%") { if percent {
val := atof(str[:len(str)-1]) val = atof(str[:len(str)-1])
if val < 0 { if val < 0 {
errorExit("margin must be non-negative") errorExit(label + " must be non-negative")
}
if val > 100 {
errorExit("margin too large")
}
} else {
val := atoi(str)
if val < 0 {
errorExit("margin must be non-negative")
}
} }
return str if val > maxPercent {
errorExit(fmt.Sprintf("%s too large (max: %d%%)", label, int(maxPercent)))
}
} else {
if strings.Contains(str, ".") {
errorExit(label + " (without %) must be a non-negative integer")
}
val = float64(atoi(str))
if val < 0 {
errorExit(label + " must be non-negative")
}
}
return sizeSpec{val, percent}
}
func parsePreviewWindow(opts *previewOpts, input string) {
layout := input
if strings.HasSuffix(layout, ":hidden") {
opts.hidden = true
layout = strings.TrimSuffix(layout, ":hidden")
}
tokens := strings.Split(layout, ":")
if len(tokens) == 0 || len(tokens) > 2 {
errorExit("invalid window layout: " + input)
}
if len(tokens) > 1 {
opts.size = parseSize(tokens[1], 99, "window size")
} else {
opts.size = sizeSpec{50, true}
}
if !opts.size.percent && opts.size.size > 0 {
// Adjust size for border
opts.size.size += 2
}
switch tokens[0] {
case "up":
opts.position = posUp
case "down":
opts.position = posDown
case "left":
opts.position = posLeft
case "right":
opts.position = posRight
default:
errorExit("invalid window position: " + input)
}
}
func parseMargin(margin string) [4]sizeSpec {
margins := strings.Split(margin, ",")
checked := func(str string) sizeSpec {
return parseSize(str, 49, "margin")
} }
switch len(margins) { switch len(margins) {
case 1: case 1:
m := checked(margins[0]) m := checked(margins[0])
return [4]string{m, m, m, m} return [4]sizeSpec{m, m, m, m}
case 2: case 2:
tb := checked(margins[0]) tb := checked(margins[0])
rl := checked(margins[1]) rl := checked(margins[1])
return [4]string{tb, rl, tb, rl} return [4]sizeSpec{tb, rl, tb, rl}
case 3: case 3:
t := checked(margins[0]) t := checked(margins[0])
rl := checked(margins[1]) rl := checked(margins[1])
b := checked(margins[2]) b := checked(margins[2])
return [4]string{t, rl, b, rl} return [4]sizeSpec{t, rl, b, rl}
case 4: case 4:
return [4]string{ return [4]sizeSpec{
checked(margins[0]), checked(margins[1]), checked(margins[0]), checked(margins[1]),
checked(margins[2]), checked(margins[3])} checked(margins[2]), checked(margins[3])}
default: default:
@ -858,6 +943,13 @@ func parseOptions(opts *Options, allArgs []string) {
case "--header-lines": case "--header-lines":
opts.HeaderLines = atoi( opts.HeaderLines = atoi(
nextString(allArgs, &i, "number of header lines required")) nextString(allArgs, &i, "number of header lines required"))
case "--preview":
opts.Preview.command = nextString(allArgs, &i, "preview command required")
case "--no-preview":
opts.Preview.command = ""
case "--preview-window":
parsePreviewWindow(&opts.Preview,
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]]"))
case "--no-margin": case "--no-margin":
opts.Margin = defaultMargin() opts.Margin = defaultMargin()
case "--margin": case "--margin":
@ -900,6 +992,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Header = strLines(value) opts.Header = strLines(value)
} else if match, value := optString(arg, "--header-lines="); match { } else if match, value := optString(arg, "--header-lines="); match {
opts.HeaderLines = atoi(value) opts.HeaderLines = atoi(value)
} else if match, value := optString(arg, "--preview="); match {
opts.Preview.command = value
} else if match, value := optString(arg, "--preview-window="); match {
parsePreviewWindow(&opts.Preview, value)
} else if match, value := optString(arg, "--margin="); match { } else if match, value := optString(arg, "--margin="); match {
opts.Margin = parseMargin(value) opts.Margin = parseMargin(value)
} else if match, value := optString(arg, "--tabstop="); match { } else if match, value := optString(arg, "--tabstop="); match {

View File

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

View File

@ -1228,6 +1228,28 @@ class TestGoFZF < TestBase
assert_equal '3', readonce.chomp assert_equal '3', readonce.chomp
end end
def test_preview
tmux.send_keys %[seq 1000 | #{FZF} --preview 'echo {{}-{}}' --bind ?:toggle-preview], :Enter
tmux.until { |lines| lines[1].include?(' {1-1}') }
tmux.send_keys '555'
tmux.until { |lines| lines[1].include?(' {555-555}') }
tmux.send_keys '?'
tmux.until { |lines| !lines[1].include?(' {555-555}') }
tmux.send_keys '?'
tmux.until { |lines| lines[1].include?(' {555-555}') }
end
def test_preview_hidden
tmux.send_keys %[seq 1000 | #{FZF} --preview 'echo {{}-{}}' --preview-window down:1:hidden --bind ?:toggle-preview], :Enter
tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys '?'
tmux.until { |lines| lines[-2].include?(' {1-1}') }
tmux.send_keys '555'
tmux.until { |lines| lines[-2].include?(' {555-555}') }
tmux.send_keys '?'
tmux.until { |lines| lines[-1] == '> 555' }
end
private private
def writelines path, lines def writelines path, lines
File.unlink path while File.exists? path File.unlink path while File.exists? path