summaryrefslogtreecommitdiffstats
path: root/src/terminal.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/terminal.go')
-rw-r--r--src/terminal.go431
1 files changed, 282 insertions, 149 deletions
diff --git a/src/terminal.go b/src/terminal.go
index 0519836f..bdbc6bba 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -2,12 +2,13 @@ package fzf
import (
"bufio"
+ "context"
"encoding/json"
"fmt"
"io"
"math"
+ "net"
"os"
- "os/exec"
"os/signal"
"regexp"
"sort"
@@ -49,9 +50,7 @@ var placeholder *regexp.Regexp
var whiteSuffix *regexp.Regexp
var offsetComponentRegex *regexp.Regexp
var offsetTrimCharsRegex *regexp.Regexp
-var activeTempFiles []string
var passThroughRegex *regexp.Regexp
-var actionTypeRegex *regexp.Regexp
const clearCode string = "\x1b[2J"
@@ -63,7 +62,6 @@ func init() {
whiteSuffix = regexp.MustCompile(`\s*$`)
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
- activeTempFiles = []string{}
// Parts of the preview output that should be passed through to the terminal
// * https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it
@@ -113,6 +111,16 @@ func (s *resumableState) Set(flag bool) {
}
}
+type commandSpec struct {
+ command string
+ tempFiles []string
+}
+
+type quitSignal struct {
+ code int
+ err error
+}
+
type previewer struct {
version int64
lines []string
@@ -227,6 +235,7 @@ type Terminal struct {
printQuery bool
history *History
cycle bool
+ highlightLine bool
headerVisible bool
headerFirst bool
headerLines int
@@ -242,9 +251,11 @@ type Terminal struct {
unicode bool
listenAddr *listenAddress
listenPort *int
+ listener net.Listener
listenUnsafe bool
borderShape tui.BorderShape
cleanExit bool
+ executor *util.Executor
paused bool
border tui.Window
window tui.Window
@@ -259,7 +270,7 @@ type Terminal struct {
hasResizeActions bool
triggerLoad bool
reading bool
- running bool
+ running *util.AtomicBool
failed *string
jumping jumpMode
jumpLabels string
@@ -278,12 +289,12 @@ type Terminal struct {
previewBox *util.EventBox
eventBox *util.EventBox
mutex sync.Mutex
- initFunc func()
+ initFunc func() error
prevLines []itemLine
suppress bool
sigstop bool
startChan chan fitpad
- killChan chan int
+ killChan chan bool
serverInputChan chan []*action
serverOutputChan chan string
eventChan chan tui.Event
@@ -298,6 +309,8 @@ type Terminal struct {
areaLines int
areaColumns int
forcePreview bool
+ clickHeaderLine int
+ clickHeaderColumn int
}
type selectedItem struct {
@@ -338,6 +351,7 @@ const (
reqPreviewRefresh
reqPreviewDelayed
reqQuit
+ reqFatal
)
type action struct {
@@ -367,6 +381,7 @@ const (
actCancel
actChangeBorderLabel
actChangeHeader
+ actChangeMulti
actChangePreviewLabel
actChangePrompt
actChangeQuery
@@ -377,6 +392,7 @@ const (
actDeleteChar
actDeleteCharEof
actEndOfLine
+ actFatal
actForwardChar
actForwardWord
actKillLine
@@ -497,7 +513,7 @@ type placeholderFlags struct {
type searchRequest struct {
sort bool
sync bool
- command *string
+ command *commandSpec
environ []string
changed bool
}
@@ -508,6 +524,7 @@ type previewRequest struct {
pwindowSize tui.TermSize
scrollOffset int
list []*Item
+ env []string
}
type previewResult struct {
@@ -534,6 +551,7 @@ func defaultKeymap() map[tui.Event][]*action {
keymap[e] = toActions(a)
}
+ add(tui.Fatal, actFatal)
add(tui.Invalid, actInvalid)
add(tui.CtrlA, actBeginningOfLine)
add(tui.CtrlB, actBackwardChar)
@@ -639,7 +657,7 @@ func evaluateHeight(opts *Options, termHeight int) int {
}
// NewTerminal returns new Terminal object
-func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
+func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor) (*Terminal, error) {
input := trimQuery(opts.Query)
var delay time.Duration
if opts.Tac {
@@ -657,11 +675,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
}
var renderer tui.Renderer
fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100)
+ var err error
if fullscreen {
if tui.HasFullscreenRenderer() {
renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
} else {
- renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
+ renderer, err = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
true, func(h int) int { return h })
}
} else {
@@ -677,7 +696,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
effectiveMinHeight += borderLines(opts.BorderShape)
return util.Min(termHeight, util.Max(evaluateHeight(opts, termHeight), effectiveMinHeight))
}
- renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
+ renderer, err = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
+ }
+ if err != nil {
+ return nil, err
}
wordRubout := "[^\\pL\\pN][\\pL\\pN]"
wordNext := "[\\pL\\pN][^\\pL\\pN]|(.$)"
@@ -690,6 +712,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
for key, action := range opts.Keymap {
keymapCopy[key] = action
}
+
t := Terminal{
initDelay: delay,
infoStyle: opts.InfoStyle,
@@ -735,8 +758,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
previewLabel: nil,
previewLabelOpts: opts.PreviewLabel,
cleanExit: opts.ClearOnExit,
+ executor: executor,
paused: opts.Phony,
cycle: opts.Cycle,
+ highlightLine: opts.CursorLine,
headerVisible: true,
headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines,
@@ -750,7 +775,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
hasLoadActions: false,
triggerLoad: false,
reading: true,
- running: true,
+ running: util.NewAtomicBool(true),
failed: nil,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
@@ -771,12 +796,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
slab: util.MakeSlab(slab16Size, slab32Size),
theme: opts.Theme,
startChan: make(chan fitpad, 1),
- killChan: make(chan int),
+ killChan: make(chan bool),
serverInputChan: make(chan []*action, 100),
serverOutputChan: make(chan string),
eventChan: make(chan tui.Event, 6), // (load + result + zero|one) | (focus) | (resize) | (GetChar)
tui: renderer,
- initFunc: func() { renderer.Init() },
+ initFunc: func() error { return renderer.Init() },
executing: util.NewAtomicBool(false),
lastAction: actStart,
lastFocus: minItem.Index()}
@@ -828,14 +853,15 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
if t.listenAddr != nil {
- port, err := startHttpServer(*t.listenAddr, t.serverInputChan, t.serverOutputChan)
+ listener, port, err := startHttpServer(*t.listenAddr, t.serverInputChan, t.serverOutputChan)
if err != nil {
- errorExit(err.Error())
+ return nil, err
}
+ t.listener = listener
t.listenPort = &port
}
- return &t
+ return &t, nil
}
func (t *Terminal) environ() []string {
@@ -854,6 +880,9 @@ func (t *Terminal) environ() []string {
env = append(env, fmt.Sprintf("FZF_SELECT_COUNT=%d", len(t.selected)))
env = append(env, fmt.Sprintf("FZF_LINES=%d", t.areaLines))
env = append(env, fmt.Sprintf("FZF_COLUMNS=%d", t.areaColumns))
+ env = append(env, fmt.Sprintf("FZF_POS=%d", util.Min(t.merger.Length(), t.cy+1)))
+ env = append(env, fmt.Sprintf("FZF_CLICK_HEADER_LINE=%d", t.clickHeaderLine))
+ env = append(env, fmt.Sprintf("FZF_CLICK_HEADER_COLUMN=%d", t.clickHeaderColumn))
return env
}
@@ -1067,7 +1096,7 @@ func (t *Terminal) UpdateProgress(progress float32) {
}
// UpdateList updates Merger to display the list
-func (t *Terminal) UpdateList(merger *Merger) {
+func (t *Terminal) UpdateList(merger *Merger, triggerResultEvent bool) {
t.mutex.Lock()
prevIndex := minItem.Index()
reset := t.revision != merger.Revision()
@@ -1118,7 +1147,7 @@ func (t *Terminal) UpdateList(merger *Merger) {
t.eventChan <- one
}
}
- if t.hasResultActions {
+ if triggerResultEvent && t.hasResultActions {
t.eventChan <- tui.Result.AsEvent()
}
}
@@ -1285,7 +1314,8 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
t.pborder.Close()
t.pborder = nil
}
- if t.pwindow != nil {
+ hadPreviewWindow := t.hasPreviewWindow()
+ if hadPreviewWindow {
t.pwindow.Close()
t.pwindow = nil
}
@@ -1384,6 +1414,9 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
pwidth = util.Max(0, pwidth)
pheight = util.Max(0, pheight)
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, true, noBorder)
+ if !hadPreviewWindow {
+ t.pwindow.Erase()
+ }
}
verticalPad := 2
minPreviewHeight := 3
@@ -1449,6 +1482,14 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
// Add a 1-column margin between the preview window and the main window
t.window = t.tui.NewWindow(
marginInt[0], marginInt[3]+pwidth+1, width-pwidth-1, height, false, noBorder)
+
+ // Clear characters on the margin
+ // fzf --bind 'space:preview(seq 100)' --preview-window left,1
+ for y := 0; y < height; y++ {
+ t.window.Move(y, -1)
+ t.window.Print(" ")
+ }
+
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
} else {
t.window = t.tui.NewWindow(
@@ -1483,10 +1524,6 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
// Print border label
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false)
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.previewOpts.border, false)
-
- for i := 0; i < t.window.Height(); i++ {
- t.window.MoveAndClear(i, 0)
- }
}
func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape, redrawBorder bool) {
@@ -1642,7 +1679,7 @@ func (t *Terminal) printInfo() {
case infoDefault:
t.move(line+1, 0, t.separatorLen == 0)
printSpinner()
- t.move(line+1, 2, false)
+ t.window.Print(" ") // Margin
pos = 2
case infoRight:
t.move(line+1, 0, false)
@@ -1707,14 +1744,17 @@ func (t *Terminal) printInfo() {
printSeparator(fillLength, true)
}
t.window.CPrint(tui.ColInfo, output)
+ t.window.Print(" ") // Margin
return
}
if t.infoStyle == infoInlineRight {
if len(t.infoPrefix) == 0 {
- pos = util.Max(pos, t.window.Width()-util.StringWidth(output)-3)
+ t.move(line, pos, false)
+ newPos := util.Max(pos, t.window.Width()-util.StringWidth(output)-3)
+ t.window.Print(strings.Repeat(" ", newPos-pos))
+ pos = newPos
if pos < t.window.Width() {
- t.move(line, pos, false)
printSpinner()
pos++
}
@@ -1862,7 +1902,7 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool, bar b
t.window.CPrint(tui.ColCurrentCursor, label)
}
if selected {
- t.window.CPrint(tui.ColCurrentSelected, t.marker)
+ t.window.CPrint(tui.ColCurrentMarker, t.marker)
} else {
t.window.CPrint(tui.ColCurrentSelectedEmpty, t.markerEmpty)
}
@@ -1874,15 +1914,36 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool, bar b
t.window.CPrint(tui.ColCursor, label)
}
if selected {
- t.window.CPrint(tui.ColSelected, t.marker)
+ t.window.CPrint(tui.ColMarker, t.marker)
} else {
t.window.Print(t.markerEmpty)
}
- newLine.width = t.printHighlighted(result, tui.ColNormal, tui.ColMatch, false, true)
+ var base, match tui.ColorPair
+ if selected {
+ base = tui.ColSelected
+ match = tui.ColSelectedMatch
+ } else {
+ base = tui.ColNormal
+ match = tui.ColMatch
+ }
+ newLine.width = t.printHighlighted(result, base, match, false, true)
}
- fillSpaces := prevLine.width - newLine.width
- if fillSpaces > 0 {
- t.window.Print(strings.Repeat(" ", fillSpaces))
+ if (current || selected) && t.highlightLine {
+ color := tui.ColSelected
+ if current {
+ color = tui.ColCurrent
+ }
+ maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
+ fillSpaces := maxWidth - newLine.width
+ newLine.width = maxWidth
+ if fillSpaces > 0 {
+ t.window.CPrint(color, strings.Repeat(" ", fillSpaces))
+ }
+ } else {
+ fillSpaces := prevLine.width - newLine.width
+ if fillSpaces > 0 {
+ t.window.Print(strings.Repeat(" ", fillSpaces))
+ }
}
printBar()
t.prevLines[i] = newLine
@@ -2085,12 +2146,11 @@ func (t *Terminal) renderPreviewArea(unchanged bool) {
}
height := t.pwindow.Height()
- header := []string{}
body := t.previewer.lines
headerLines := t.previewOpts.headerLines
// Do not enable preview header lines if it's value is too large
if headerLines > 0 && headerLines < util.Min(len(body), height) {
- header = t.previewer.lines[0:headerLines]
+ header := t.previewer.lines[0:headerLines]
body = t.previewer.lines[headerLines:]
// Always redraw header
t.renderPreviewText(height, header, 0, false)
@@ -2214,9 +2274,8 @@ Loop:
t.pwindow.Move(height-1, maxWidth-1)
t.previewed.filled = true
break Loop
- } else {
- t.pwindow.MoveAndClear(y+requiredLines, 0)
}
+ t.pwindow.MoveAndClear(y+requiredLines, 0)
}
}
@@ -2467,8 +2526,6 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
case 'q':
flags.forceUpdate = true
// query flag is not skipped
- default:
- break
}
}
@@ -2491,26 +2548,6 @@ func hasPreviewFlags(template string) (slot bool, plus bool, forceUpdate bool) {
return
}
-func writeTemporaryFile(data []string, printSep string) string {
- f, err := os.CreateTemp("", "fzf-preview-*")
- if err != nil {
- errorExit("Unable to create temporary file")
- }
- defer f.Close()
-
- f.WriteString(strings.Join(data, printSep))
- f.WriteString(printSep)
- activeTempFiles = append(activeTempFiles, f.Name())
- return f.Name()
-}
-
-func cleanTemporaryFiles() {
- for _, filename := range activeTempFiles {
- os.Remove(filename)
- }
- activeTempFiles = []string{}
-}
-
type replacePlaceholderParams struct {
template string
stripAnsi bool
@@ -2521,9 +2558,10 @@ type replacePlaceholderParams struct {
allItems []*Item
lastAction actionType
prompt string
+ executor *util.Executor
}
-func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string {
+func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) (string, []string) {
return replacePlaceholder(replacePlaceholderParams{
template: template,
stripAnsi: t.ansi,
@@ -2534,6 +2572,7 @@ func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input str
allItems: list,
lastAction: t.lastAction,
prompt: t.promptString,
+ executor: t.executor,
})
}
@@ -2543,8 +2582,9 @@ func (t *Terminal) evaluateScrollOffset() int {
}
// We only need the current item to calculate the scroll offset
- offsetExpr := offsetTrimCharsRegex.ReplaceAllString(
- t.replacePlaceholder(t.previewOpts.scroll, false, "", []*Item{t.currentItem(), nil}), "")
+ replaced, tempFiles := t.replacePlaceholder(t.previewOpts.scroll, false, "", []*Item{t.currentItem(), nil})
+ removeFiles(tempFiles)
+ offsetExpr := offsetTrimCharsRegex.ReplaceAllString(replaced, "")
atoi := func(s string) int {
n, e := strconv.Atoi(s)
@@ -2572,7 +2612,8 @@ func (t *Terminal) evaluateScrollOffset() int {
return util.Max(0, base)
}
-func replacePlaceholder(params replacePlaceholderParams) string {
+func replacePlaceholder(params replacePlaceholderParams) (string, []string) {
+ tempFiles := []string{}
current := params.allItems[:1]
selected := params.allItems[1:]
if current[0] == nil {
@@ -2583,7 +2624,7 @@ func replacePlaceholder(params replacePlaceholderParams) string {
}
// replace placeholders one by one
- return placeholder.ReplaceAllStringFunc(params.template, func(match string) string {
+ replaced := placeholder.ReplaceAllStringFunc(params.template, func(match string) string {
escaped, match, flags := parsePlaceholder(match)
// this function implements the effects a placeholder has on items
@@ -2594,7 +2635,7 @@ func replacePlaceholder(params replacePlaceholderParams) string {
case escaped:
return match
case match == "{q}" || match == "{fzf:query}":
- return quoteEntry(params.query)
+ return params.executor.QuoteEntry(params.query)
case match == "{}":
replace = func(item *Item) string {
switch {
@@ -2607,13 +2648,13 @@ func replacePlaceholder(params replacePlaceholderParams) string {
case flags.file:
return item.AsString(params.stripAnsi)
default:
- return quoteEntry(item.AsString(params.stripAnsi))
+ return params.executor.QuoteEntry(item.AsString(params.stripAnsi))
}
}
case match == "{fzf:action}":
return params.lastAction.Name()
case match == "{fzf:prompt}":
- return quoteEntry(params.prompt)
+ return params.executor.QuoteEntry(params.prompt)
default:
// token type and also failover (below)
rangeExpressions := strings.Split(match[1:len(match)-1], ",")
@@ -2647,7 +2688,7 @@ func replacePlaceholder(params replacePlaceholderParams) string {
str = strings.TrimSpace(str)
}
if !flags.file {
- str = quoteEntry(str)
+ str = params.executor.QuoteEntry(str)
}
return str
}
@@ -2666,10 +2707,14 @@ func replacePlaceholder(params replacePlaceholderParams) string {
}
if flags.file {
- return writeTemporaryFile(replacements, params.printsep)
+ file := writeTemporaryFile(replacements, params.printsep)
+ tempFiles = append(tempFiles, file)
+ return file
}
return strings.Join(replacements, " ")
})
+
+ return replaced, tempFiles
}
func (t *Terminal) redraw() {
@@ -2686,8 +2731,8 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
if !valid && !capture {
return line
}
- command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
- cmd := util.ExecCommand(command, false)
+ command, tempFiles := t.replacePlaceholder(template, forcePlus, string(t.input), list)
+ cmd := t.executor.ExecCommand(command, false)
cmd.Env = t.environ()
t.executing.Set(true)
if !background {
@@ -2717,7 +2762,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
}
}
t.executing.Set(false)
- cleanTemporaryFiles()
+ removeFiles(tempFiles)
return line
}
@@ -2816,18 +2861,18 @@ func (t *Terminal) toggleItem(item *Item) bool {
return true
}
-func (t *Terminal) killPreview(code int) {
+func (t *Terminal) killPreview() {
select {
- case t.killChan <- code:
+ case t.killChan <- true:
default:
- if code != exitCancel {
- t.eventBox.Set(EvtQuit, code)
- }
}
}
func (t *Terminal) cancelPreview() {
- t.killPreview(exitCancel)
+ select {
+ case t.killChan <- false:
+ default:
+ }
}
func (t *Terminal) pwindowSize() tui.TermSize {
@@ -2851,7 +2896,7 @@ func (t *Terminal) currentIndex() int32 {
}
// Loop is called to start Terminal I/O
-func (t *Terminal) Loop() {
+func (t *Terminal) Loop() error {
// prof := profile.Start(profile.ProfilePath("/tmp/"))
fitpad := <-t.startChan
fit := fitpad.fit
@@ -2875,14 +2920,23 @@ func (t *Terminal) Loop() {
return util.Min(termHeight, contentHeight+pad)
})
}
+
+ // Context
+ ctx, cancel := context.WithCancel(context.Background())
+
{ // Late initialization
intChan := make(chan os.Signal, 1)
signal.Notify(intChan, os.Interrupt, syscall.SIGTERM)
go func() {
- for s := range intChan {
- // Don't quit by SIGINT while executing because it should be for the executing command and not for fzf itself
- if !(s == os.Interrupt && t.executing.Get()) {
- t.reqBox.Set(reqQuit, nil)
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case s := <-intChan:
+ // Don't quit by SIGINT while executing because it should be for the executing command and not for fzf itself
+ if !(s == os.Interrupt && t.executing.Get()) {
+ t.reqBox.Set(reqQuit, nil)
+ }
}
}
}()
@@ -2891,8 +2945,12 @@ func (t *Terminal) Loop() {
notifyOnCont(contChan)
go func() {
for {
- <-contChan
- t.reqBox.Set(reqReinit, nil)
+ select {
+ case <-ctx.Done():
+ return
+ case <-contChan:
+ t.reqBox.Set(reqReinit, nil)
+ }
}
}()
@@ -2901,16 +2959,26 @@ func (t *Terminal) Loop() {
notifyOnResize(resizeChan) // Non-portable
go func() {
for {
- <-resizeChan
- t.reqBox.Set(reqResize, nil)
+ select {
+ case <-ctx.Done():
+ return
+ case <-resizeChan:
+ t.reqBox.Set(reqResize, nil)
+ }
}
}()
}
t.mutex.Lock()
- t.initFunc()
+ if err := t.initFunc(); err != nil {
+ t.mutex.Unlock()
+ cancel()
+ t.eventBox.Set(EvtQuit, quitSignal{ExitError, err})
+ return err
+ }
t.termSize = t.tui.Size()
t.resizeWindows(false)
+ t.window.Erase()
t.printPrompt()
t.printInfo()
t.printHeader()
@@ -2924,7 +2992,7 @@ func (t *Terminal) Loop() {
// Keep the spinner spinning
go func() {
- for {
+ for t.running.Get() {
t.mutex.Lock()
reading := t.reading
t.mutex.Unlock()
@@ -2939,15 +3007,20 @@ func (t *Terminal) Loop() {
if t.hasPreviewer() {
go func() {
var version int64
+ stop := false
for {
var items []*Item
var commandTemplate string
var pwindow tui.Window
var pwindowSize tui.TermSize
+ var env []string
initialOffset := 0
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
switch req {
+ case reqQuit:
+ stop = true
+ return
case reqPreviewEnqueue:
request := value.(previewRequest)
commandTemplate = request.template
@@ -2955,17 +3028,20 @@ func (t *Terminal) Loop() {
items = request.list
pwindow = request.pwindow
pwindowSize = request.pwindowSize
+ env = request.env
}
}
events.Clear()
})
+ if stop {
+ break
+ }
version++
// We don't display preview window if no match
if items[0] != nil {
_, query := t.Input()
- command := t.replacePlaceholder(commandTemplate, false, string(query), items)
- cmd := util.ExecCommand(command, true)
- env := t.environ()
+ command, tempFiles := t.replacePlaceholder(commandTemplate, false, string(query), items)
+ cmd := t.executor.ExecCommand(command, true)
if pwindowSize.Lines > 0 {
lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines)
columns := fmt.Sprintf("COLUMNS=%d", pwindowSize.Columns)
@@ -3050,12 +3126,13 @@ func (t *Terminal) Loop() {
Loop:
for {
select {
+ case <-ctx.Done():
+ break Loop
case <-timer.C:
t.reqBox.Set(reqPreviewDelayed, version)
- case code := <-t.killChan:
- if code != exitCancel {
+ case immediately := <-t.killChan:
+ if immediately {
util.KillCommand(cmd)
- t.eventBox.Set(EvtQuit, code)
} else {
// We can immediately kill a long-running preview program
// once we started rendering its partial output
@@ -3085,12 +3162,11 @@ func (t *Terminal) Loop() {
finishChan <- true // Tell Goroutine 3 to stop
<-reapChan // Goroutine 2 and 3 finished
<-reapChan
+ removeFiles(tempFiles)
} else {
// Failed to start the command. Report the error immediately.
t.reqBox.Set(reqPreviewDisplay, previewResult{version, []string{err.Error()}, 0, ""})
}
-
- cleanTemporaryFiles()
} else {
t.reqBox.Set(reqPreviewDisplay, previewResult{version, nil, 0, ""})
}
@@ -3102,19 +3178,25 @@ func (t *Terminal) Loop() {
if len(command) > 0 && t.canPreview() {
_, list := t.buildPlusList(command, false)
t.cancelPreview()
- t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list})
+ t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list, t.environ()})
}
}
go func() {
- var focusedIndex int32 = minItem.Index()
+ var focusedIndex = minItem.Index()
var version int64 = -1
running := true
- code := exitError
+ code := ExitError
exit := func(getCode func() int) {
+ if t.hasPreviewer() {
+ t.previewBox.Set(reqQuit, nil)
+ }
+ if t.listener != nil {
+ t.listener.Close()
+ }
t.tui.Close()
code = getCode()
- if code <= exitNoMatch && t.history != nil {
+ if code <= ExitNoMatch && t.history != nil {
t.history.append(string(t.input))
}
running = false
@@ -3182,9 +3264,9 @@ func (t *Terminal) Loop() {
case reqClose:
exit(func() int {
if t.output() {
- return exitOk
+ return ExitOk
}
- return exitNoMatch
+ return ExitNoMatch
})
return
case reqPreviewDisplay:
@@ -3212,11 +3294,14 @@ func (t *Terminal) Loop() {
case reqPrintQuery:
exit(func() int {
t.printer(string(t.input))
- return exitOk
+ return ExitOk
})
return
case reqQuit:
- exit(func() int { return exitInterrupt })
+ exit(func() int { return ExitInterrupt })
+ return
+ case reqFatal:
+ exit(func() int { return ExitError })
return
}
}
@@ -3224,8 +3309,11 @@ func (t *Terminal) Loop() {
t.mutex.Unlock()
})
}
- // prof.Stop()
- t.killPreview(code)
+
+ t.eventBox.Set(EvtQuit, quitSignal{code, nil})
+ t.running.Set(false)
+ t.killPreview()
+ cancel()
}()
looping := true
@@ -3235,8 +3323,16 @@ func (t *Terminal) Loop() {
barrier := make(chan bool)
go func() {
for {
- <-barrier
- t.eventChan <- t.tui.GetChar()
+ select {
+ case <-ctx.Done():
+ return
+ case <-barrier:
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case t.eventChan <- t.tui.GetChar():
+ }
}
}()
previewDraggingPos := -1
@@ -3244,7 +3340,7 @@ func (t *Terminal) Loop() {
pbarDragging := false
wasDown := false
for looping {
- var newCommand *string
+ var newCommand *commandSpec
var reloadSync bool
changed := false
beof := false
@@ -3332,7 +3428,7 @@ func (t *Terminal) Loop() {
t.pressed = ret
t.reqBox.Set(reqClose, nil)
t.mutex.Unlock()
- return
+ return nil
}
}
@@ -3341,8 +3437,7 @@ func (t *Terminal) Loop() {
}
var doAction func(*action) bool
- var doActions func(actions []*action) bool
- doActions = func(actions []*action) bool {
+ doActions := func(actions []*action) bool {
for iter := 0; iter <= maxFocusEvents; iter++ {
currentIndex := t.currentIndex()
for _, action := range actions {
@@ -3370,28 +3465,23 @@ func (t *Terminal) Loop() {
case actBecome:
valid, list := t.buildPlusList(a.a, false)
if valid {
- command := t.replacePlaceholder(a.a, false, string(t.input), list)
- shell := os.Getenv("SHELL")
- if len(shell) == 0 {
- shell = "sh"
- }
- shellPath, err := exec.LookPath(shell)
- if err == nil {
- t.tui.Close()
- if t.history != nil {
- t.history.append(string(t.input))
- }
- /*
- FIXME: It is not at all clear why this is required.
- The following command will report 'not a tty', unless we open
- /dev/tty *twice* after closing the standard input for 'reload'
- in Reader.terminate().
- : | fzf --bind 'start:reload:ls' --bind 'enter:become:tty'
- */
- tui.TtyIn()
- util.SetStdin(tui.TtyIn())
- syscall.Exec(shellPath, []string{shell, "-c", command}, os.Environ())
+ // We do not remove temp files in this case
+ command, _ := t.replacePlaceholder(a.a, false, string(t.input), list)
+ t.tui.Close()
+ if t.history != nil {
+ t.history.append(string(t.input))
}
+
+ /*
+ FIXME: It is not at all clear why this is required.
+ The following command will report 'not a tty', unless we open
+ /dev/tty *twice* after closing the standard input for 'reload'
+ in Reader.terminate().
+
+ while : | fzf --bind 'start:reload:ls' --bind 'load:become:tty'; do echo; done
+ */
+ tui.TtyIn()
+ t.executor.Become(tui.TtyIn(), t.environ(), command)
}
case actExecute, actExecuteSilent:
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false)
@@ -3418,7 +3508,7 @@ func (t *Terminal) Loop() {
if valid {
t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue,
- previewRequest{t.previewOpts.command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list})
+ previewRequest{t.previewOpts.command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list, t.environ()})
}
} else {
// Discard the preview content so that it won't accidentally appear
@@ -3489,6 +3579,19 @@ func (t *Terminal) Loop() {
}
case actPrintQuery:
req(reqPrintQuery)
+ case actChangeMulti:
+ multi := t.multi
+ if a.a == "" {
+ multi = maxMulti
+ } else if n, e := strconv.Atoi(a.a); e == nil && n >= 0 {
+ multi = n
+ }
+ if t.multi > 0 && multi != t.multi {
+ t.selected = make(map[int32]selectedItem)
+ t.version++
+ }
+ t.multi = multi
+ req(reqList, reqInfo)
case actChangeQuery:
t.input = []rune(a.a)
t.cx = len(t.input)
@@ -3519,8 +3622,9 @@ func (t *Terminal) Loop() {
}
case actTransform:
body := t.executeCommand(a.a, false, true, true, false)
- actions := parseSingleActionList(strings.Trim(body, "\r\n"), func(message string) {})
- return doActions(actions)
+ if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil {
+ return doActions(actions)
+ }
case actTransformBorderLabel:
label := t.executeCommand(a.a, false, true, true, true)
t.borderLabelOpts.label = label
@@ -3552,6 +3656,8 @@ func (t *Terminal) Loop() {
t.input = current.text.ToRunes()
t.cx = len(t.input)
}
+ case actFatal:
+ req(reqFatal)
case actAbort:
req(reqQuit)
case actDeleteChar:
@@ -3977,10 +4083,10 @@ func (t *Terminal) Loop() {
}
if me.Down {
- mx = util.Constrain(mx-t.promptLen, 0, len(t.input))
- if my == t.promptLine() && mx >= 0 {
+ mxCons := util.Constrain(mx-t.promptLen, 0, len(t.input))
+ if my == t.promptLine() && mxCons >= 0 {
// Prompt
- t.cx = mx + t.xoffset
+ t.cx = mxCons + t.xoffset
} else if my >= min {
t.vset(t.offset + my - min)
req(reqList)
@@ -3995,6 +4101,29 @@ func (t *Terminal) Loop() {
}
}
return doActions(actionsFor(evt))
+ } else if t.headerVisible {
+ // Header
+ numLines := t.visibleHeaderLines()
+ lineOffset := 0
+ if !t.headerFirst {
+ // offset for info line
+ if t.noSeparatorLine() {
+ lineOffset = 1
+ } else {
+ lineOffset = 2
+ }
+ }
+ my -= lineOffset
+ mx -= 2 // offset gutter
+ if my >= 0 && my < numLines && mx >= 0 {
+ if t.layout == layoutReverse {
+ t.clickHeaderLine = my + 1
+ } else {
+ t.clickHeaderLine = numLines - my
+ }
+ t.clickHeaderColumn = mx + 1
+ return doActions(actionsFor(tui.ClickHeader))
+ }
}
}
case actReload, actReloadSync:
@@ -4009,21 +4138,23 @@ func (t *Terminal) Loop() {
valid = !slot || forceUpdate
}
if valid {
- command := t.replacePlaceholder(a.a, false, string(t.input), list)
- newCommand = &command
+ command, tempFiles := t.replacePlaceholder(a.a, false, string(t.input), list)
+ newCommand = &commandSpec{command, tempFiles}
reloadSync = a.t == actReloadSync
t.reading = true
}
case actUnbind:
- keys := parseKeyChords(a.a, "PANIC")
- for key := range keys {
- delete(t.keymap, key)
+ if keys, err := parseKeyChords(a.a, "PANIC"); err == nil {
+ for key := range keys {
+ delete(t.keymap, key)
+ }
}
case actRebind:
- keys := parseKeyChords(a.a, "PANIC")
- for key := range keys {
- if originalAction, found := t.keymapOrg[key]; found {
- t.keymap[key] = originalAction
+ if keys, err := parseKeyChords(a.a, "PANIC"); err == nil {
+ for key := range keys {
+ if originalAction, found := t.keymapOrg[key]; found {
+ t.keymap[key] = originalAction
+ }
}
}
case actChangePreview:
@@ -4037,6 +4168,7 @@ func (t *Terminal) Loop() {
// Reset preview options and apply the additional options
t.previewOpts = t.initialPreviewOpts
+ t.previewOpts.command = currentPreviewOpts.command
// Split window options
tokens := strings.Split(a.a, "|")
@@ -4169,6 +4301,7 @@ func (t *Terminal) Loop() {
t.reqBox.Set(event, nil)
}
}
+ return nil
}
func (t *Terminal) constrain() {