diff --git a/src/curses/curses.go b/src/curses/curses.go index ca10fc7d..7a9ccd4a 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -113,7 +113,8 @@ const ( ColCursor ColSelected ColHeader - ColUser + ColBorder + ColUser // Should be the last entry ) const ( @@ -136,6 +137,7 @@ type ColorTheme struct { Cursor int16 Selected int16 Header int16 + Border int16 } type Event struct { @@ -170,6 +172,31 @@ var ( 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 { return &ColorTheme{ UseDefault: true, @@ -184,7 +211,8 @@ func EmptyTheme() *ColorTheme { Info: colUndefined, Cursor: colUndefined, Selected: colUndefined, - Header: colUndefined} + Header: colUndefined, + Border: colUndefined} } func init() { @@ -204,7 +232,8 @@ func init() { Info: C.COLOR_WHITE, Cursor: C.COLOR_RED, Selected: C.COLOR_MAGENTA, - Header: C.COLOR_CYAN} + Header: C.COLOR_CYAN, + Border: C.COLOR_BLACK} Dark256 = &ColorTheme{ UseDefault: true, Fg: 15, @@ -218,7 +247,8 @@ func init() { Info: 144, Cursor: 161, Selected: 168, - Header: 109} + Header: 109, + Border: 59} Light256 = &ColorTheme{ UseDefault: true, Fg: 15, @@ -232,7 +262,8 @@ func init() { Info: 101, Cursor: 161, Selected: 168, - Header: 31} + Header: 31, + Border: 145} } 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(ColSelected, override(baseTheme.Selected, theme.Selected), darkBG) C.init_pair(ColHeader, override(baseTheme.Header, theme.Header), bg) + C.init_pair(ColBorder, override(baseTheme.Border, theme.Border), bg) } func Close() { @@ -415,7 +447,9 @@ func mouseSequence(sz *int) Event { 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl mod := _buf[3] >= 100 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} } @@ -588,17 +622,25 @@ func GetChar() Event { return Event{Rune, r, nil} } -func Move(y int, x int) { - C.move(C.int(y), C.int(x)) +func (w *Window) Close() { + C.delwin(w.win) +} + +func (w *Window) Enclose(y int, x int) bool { + return bool(C.wenclose(w.win, C.int(y), C.int(x))) +} + +func (w *Window) Move(y int, x int) { + C.wmove(w.win, C.int(y), C.int(x)) } -func MoveAndClear(y int, x int) { - Move(y, x) - C.clrtoeol() +func (w *Window) MoveAndClear(y int, x int) { + w.Move(y, x) + C.wclrtoeol(w.win) } -func Print(text string) { - C.addstr(C.CString(strings.Map(func(r rune) rune { +func (w *Window) Print(text string) { + C.waddstr(w.win, C.CString(strings.Map(func(r rune) rune { if r < 32 { return -1 } @@ -606,11 +648,11 @@ func Print(text string) { }, text))) } -func CPrint(pair int, bold bool, text string) { +func (w *Window) CPrint(pair int, bold bool, text string) { attr := _color(pair, bold) - C.attron(attr) - Print(text) - C.attroff(attr) + C.wattron(w.win, attr) + w.Print(text) + C.wattroff(w.win, attr) } func Clear() { @@ -625,6 +667,30 @@ func 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 { key := (fg << 8) + bg if found, prs := _colorMap[key]; prs { diff --git a/src/options.go b/src/options.go index ffd57e96..2a724274 100644 --- a/src/options.go +++ b/src/options.go @@ -1,6 +1,7 @@ package fzf import ( + "fmt" "os" "regexp" "strconv" @@ -23,36 +24,47 @@ const usage = `usage: fzf [options] -n, --nth=N[,..] Comma-separated list of field index expressions for limiting search scope. Each can be a non-zero integer or a range expression ([BEGIN]..[END]). - --with-nth=N[,..] Transform item using index expressions within finder - -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) + --with-nth=N[,..] Transform the presentation of each line using + field index expressions + -d, --delimiter=STR Field delimiter regex (default: AWK-style) +s, --no-sort Do not sort the result --tac Reverse the order of the input --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply - when the scores are tied; - [length|begin|end|index] (default: length) + when the scores are tied [length|begin|end|index] + (default: length) Interface -m, --multi Enable multi-select with tab/shift-tab - --ansi Enable processing of ANSI color codes --no-mouse Disable mouse - --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors - --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) + --bind=KEYBINDS Custom key bindings. Refer to the man page. --cycle Enable cyclic scroll --no-hscroll Disable horizontal scroll --hscroll-off=COL Number of screen columns to keep to the right of the highlighted substring (default: 10) - --inline-info Display finder info inline with the query --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: '> ') - --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-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 -q, --query=STR Start the finder with the given query -1, --select-1 Automatically select the only match @@ -88,8 +100,29 @@ const ( byEnd ) -func defaultMargin() [4]string { - return [4]string{"0", "0", "0", "0"} +type sizeSpec struct { + 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 @@ -123,13 +156,14 @@ type Options struct { Expect map[int]string Keymap map[int]actionType Execmap map[int]string + Preview previewOpts PrintQuery bool ReadZero bool Sync bool History *History Header []string HeaderLines int - Margin [4]string + Margin [4]sizeSpec Tabstop int Version bool } @@ -165,6 +199,7 @@ func defaultOptions() *Options { Expect: make(map[int]string), Keymap: make(map[int]actionType), Execmap: make(map[int]string), + Preview: previewOpts{"", posRight, sizeSpec{50, true}, false}, PrintQuery: false, ReadZero: false, Sync: false, @@ -458,6 +493,8 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme theme.Match = ansi case "hl+": theme.CurrentMatch = ansi + case "border": + theme.Border = ansi case "prompt": theme.Prompt = ansi case "spinner": @@ -604,6 +641,8 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) keymap[key] = actPreviousHistory case "next-history": keymap[key] = actNextHistory + case "toggle-preview": + keymap[key] = actTogglePreview case "toggle-sort": keymap[key] = actToggleSort default: @@ -659,40 +698,86 @@ func strLines(str string) []string { return strings.Split(strings.TrimSuffix(str, "\n"), "\n") } -func parseMargin(margin string) [4]string { - margins := strings.Split(margin, ",") - checked := func(str string) string { - if strings.HasSuffix(str, "%") { - val := atof(str[:len(str)-1]) - if val < 0 { - errorExit("margin must be non-negative") - } - if val > 100 { - errorExit("margin too large") - } - } else { - val := atoi(str) - if val < 0 { - errorExit("margin must be non-negative") - } +func parseSize(str string, maxPercent float64, label string) sizeSpec { + var val float64 + percent := strings.HasSuffix(str, "%") + if percent { + val = atof(str[:len(str)-1]) + if val < 0 { + errorExit(label + " 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) { case 1: m := checked(margins[0]) - return [4]string{m, m, m, m} + return [4]sizeSpec{m, m, m, m} case 2: tb := checked(margins[0]) rl := checked(margins[1]) - return [4]string{tb, rl, tb, rl} + return [4]sizeSpec{tb, rl, tb, rl} case 3: t := checked(margins[0]) rl := checked(margins[1]) b := checked(margins[2]) - return [4]string{t, rl, b, rl} + return [4]sizeSpec{t, rl, b, rl} case 4: - return [4]string{ + return [4]sizeSpec{ checked(margins[0]), checked(margins[1]), checked(margins[2]), checked(margins[3])} default: @@ -858,6 +943,13 @@ func parseOptions(opts *Options, allArgs []string) { case "--header-lines": opts.HeaderLines = atoi( 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": opts.Margin = defaultMargin() case "--margin": @@ -900,6 +992,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Header = strLines(value) } else if match, value := optString(arg, "--header-lines="); match { 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 { opts.Margin = parseMargin(value) } else if match, value := optString(arg, "--tabstop="); match { diff --git a/src/terminal.go b/src/terminal.go index b1455e8e..ce10ee75 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -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++ } diff --git a/test/test_go.rb b/test/test_go.rb index bb35bbf0..a6d67190 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1228,6 +1228,28 @@ class TestGoFZF < TestBase assert_equal '3', readonce.chomp 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 def writelines path, lines File.unlink path while File.exists? path