Refactor the code so that fzf can be used as a library (#3769)

pull/3781/head
Junegunn Choi 4 weeks ago committed by GitHub
parent 065b9e6fb2
commit e8405f40fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -79,7 +79,6 @@ all: target/$(BINARY)
test: $(SOURCES) test: $(SOURCES)
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1) [ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \ SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
github.com/junegunn/fzf \
github.com/junegunn/fzf/src \ github.com/junegunn/fzf/src \
github.com/junegunn/fzf/src/algo \ github.com/junegunn/fzf/src/algo \
github.com/junegunn/fzf/src/tui \ github.com/junegunn/fzf/src/tui \

@ -3,14 +3,15 @@ package main
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"os"
"strings" "strings"
fzf "github.com/junegunn/fzf/src" fzf "github.com/junegunn/fzf/src"
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version string = "0.51" var version = "0.51"
var revision string = "devel" var revision = "devel"
//go:embed shell/key-bindings.bash //go:embed shell/key-bindings.bash
var bashKeyBindings []byte var bashKeyBindings []byte
@ -33,9 +34,21 @@ func printScript(label string, content []byte) {
fmt.Println("### end: " + label + " ###") fmt.Println("### end: " + label + " ###")
} }
func exit(code int, err error) {
if err != nil {
os.Stderr.WriteString(err.Error() + "\n")
}
os.Exit(code)
}
func main() { func main() {
protector.Protect() protector.Protect()
options := fzf.ParseOptions()
options, err := fzf.ParseOptions(true, os.Args[1:])
if err != nil {
exit(fzf.ExitError, err)
return
}
if options.Bash { if options.Bash {
printScript("key-bindings.bash", bashKeyBindings) printScript("key-bindings.bash", bashKeyBindings)
printScript("completion.bash", bashCompletion) printScript("completion.bash", bashCompletion)
@ -51,5 +64,19 @@ func main() {
fmt.Println("fzf_key_bindings") fmt.Println("fzf_key_bindings")
return return
} }
fzf.Run(options, version, revision) if options.Help {
fmt.Print(fzf.Usage)
return
}
if options.Version {
if len(revision) > 0 {
fmt.Printf("%s (%s)\n", version, revision)
} else {
fmt.Println(version)
}
return
}
code, err := fzf.Run(options)
exit(code, err)
} }

@ -1,174 +0,0 @@
package main
import (
"bytes"
"fmt"
"go/ast"
"go/build"
"go/importer"
"go/parser"
"go/token"
"go/types"
"io/fs"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"testing"
)
func loadPackages(t *testing.T) []*build.Package {
// If GOROOT is not set, use `go env GOROOT` to determine it since it
// performs more work than just runtime.GOROOT(). For context, running
// the tests with the "-trimpath" flag causes GOROOT to not be set.
ctxt := &build.Default
if ctxt.GOROOT == "" {
cmd := exec.Command("go", "env", "GOROOT")
out, err := cmd.CombinedOutput()
out = bytes.TrimSpace(out)
if err != nil {
t.Fatalf("error running command: %q: %v\n%s", cmd.Args, err, out)
}
ctxt.GOROOT = string(out)
}
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
var pkgs []*build.Package
seen := make(map[string]bool)
err = filepath.WalkDir(wd, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
name := d.Name()
if d.IsDir() {
if name == "" || name[0] == '.' || name[0] == '_' || name == "vendor" || name == "tmp" {
return filepath.SkipDir
}
return nil
}
if d.Type().IsRegular() && filepath.Ext(name) == ".go" && !strings.HasSuffix(name, "_test.go") {
dir := filepath.Dir(path)
if !seen[dir] {
pkg, err := ctxt.ImportDir(dir, build.ImportComment)
if err != nil {
return fmt.Errorf("%s: %s", dir, err)
}
if pkg.ImportPath == "" || pkg.ImportPath == "." {
importPath, err := filepath.Rel(wd, dir)
if err != nil {
t.Fatal(err)
}
pkg.ImportPath = filepath.ToSlash(filepath.Join("github.com/junegunn/fzf", importPath))
}
pkgs = append(pkgs, pkg)
seen[dir] = true
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
sort.Slice(pkgs, func(i, j int) bool {
return pkgs[i].ImportPath < pkgs[j].ImportPath
})
return pkgs
}
var sourceImporter = importer.ForCompiler(token.NewFileSet(), "source", nil)
func checkPackageForOsExit(t *testing.T, bpkg *build.Package, allowed map[string]int) (errOsExit bool) {
var files []*ast.File
fset := token.NewFileSet()
for _, name := range bpkg.GoFiles {
filename := filepath.Join(bpkg.Dir, name)
af, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
files = append(files, af)
}
info := types.Info{
Uses: make(map[*ast.Ident]types.Object),
}
conf := types.Config{
Importer: sourceImporter,
}
_, err := conf.Check(bpkg.Name, fset, files, &info)
if err != nil {
t.Fatal(err)
}
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
for id, obj := range info.Uses {
if obj.Pkg() != nil && obj.Pkg().Name() == "os" && obj.Name() == "Exit" {
pos := fset.Position(id.Pos())
name, err := filepath.Rel(wd, pos.Filename)
if err != nil {
t.Log(err)
name = pos.Filename
}
name = filepath.ToSlash(name)
// Check if the usage is allowed
if allowed[name] > 0 {
allowed[name]--
continue
}
t.Errorf("os.Exit referenced at: %s:%d:%d", name, pos.Line, pos.Column)
errOsExit = true
}
}
return errOsExit
}
// Enforce that src/util.Exit() is used instead of os.Exit by prohibiting
// references to it anywhere else in the fzf code base.
func TestOSExitNotAllowed(t *testing.T) {
if testing.Short() {
t.Skip("skipping: short test")
}
allowed := map[string]int{
"src/util/atexit.go": 1, // os.Exit allowed 1 time in "atexit.go"
}
var errOsExit bool
for _, pkg := range loadPackages(t) {
t.Run(pkg.ImportPath, func(t *testing.T) {
if checkPackageForOsExit(t, pkg, allowed) {
errOsExit = true
}
})
}
if t.Failed() && errOsExit {
var names []string
for name := range allowed {
names = append(names, fmt.Sprintf("%q", name))
}
sort.Strings(names)
const errMsg = `
Test failed because os.Exit was referenced outside of the following files:
%s
Use github.com/junegunn/fzf/src/util.Exit() instead to exit the program.
This is enforced because calling os.Exit() prevents the functions
registered with util.AtExit() from running.`
t.Errorf(errMsg, strings.Join(names, "\n "))
}
}

@ -37,92 +37,93 @@ func _() {
_ = x[actDeleteChar-26] _ = x[actDeleteChar-26]
_ = x[actDeleteCharEof-27] _ = x[actDeleteCharEof-27]
_ = x[actEndOfLine-28] _ = x[actEndOfLine-28]
_ = x[actForwardChar-29] _ = x[actFatal-29]
_ = x[actForwardWord-30] _ = x[actForwardChar-30]
_ = x[actKillLine-31] _ = x[actForwardWord-31]
_ = x[actKillWord-32] _ = x[actKillLine-32]
_ = x[actUnixLineDiscard-33] _ = x[actKillWord-33]
_ = x[actUnixWordRubout-34] _ = x[actUnixLineDiscard-34]
_ = x[actYank-35] _ = x[actUnixWordRubout-35]
_ = x[actBackwardKillWord-36] _ = x[actYank-36]
_ = x[actSelectAll-37] _ = x[actBackwardKillWord-37]
_ = x[actDeselectAll-38] _ = x[actSelectAll-38]
_ = x[actToggle-39] _ = x[actDeselectAll-39]
_ = x[actToggleSearch-40] _ = x[actToggle-40]
_ = x[actToggleAll-41] _ = x[actToggleSearch-41]
_ = x[actToggleDown-42] _ = x[actToggleAll-42]
_ = x[actToggleUp-43] _ = x[actToggleDown-43]
_ = x[actToggleIn-44] _ = x[actToggleUp-44]
_ = x[actToggleOut-45] _ = x[actToggleIn-45]
_ = x[actToggleTrack-46] _ = x[actToggleOut-46]
_ = x[actToggleTrackCurrent-47] _ = x[actToggleTrack-47]
_ = x[actToggleHeader-48] _ = x[actToggleTrackCurrent-48]
_ = x[actTrackCurrent-49] _ = x[actToggleHeader-49]
_ = x[actUntrackCurrent-50] _ = x[actTrackCurrent-50]
_ = x[actDown-51] _ = x[actUntrackCurrent-51]
_ = x[actUp-52] _ = x[actDown-52]
_ = x[actPageUp-53] _ = x[actUp-53]
_ = x[actPageDown-54] _ = x[actPageUp-54]
_ = x[actPosition-55] _ = x[actPageDown-55]
_ = x[actHalfPageUp-56] _ = x[actPosition-56]
_ = x[actHalfPageDown-57] _ = x[actHalfPageUp-57]
_ = x[actOffsetUp-58] _ = x[actHalfPageDown-58]
_ = x[actOffsetDown-59] _ = x[actOffsetUp-59]
_ = x[actJump-60] _ = x[actOffsetDown-60]
_ = x[actJumpAccept-61] _ = x[actJump-61]
_ = x[actPrintQuery-62] _ = x[actJumpAccept-62]
_ = x[actRefreshPreview-63] _ = x[actPrintQuery-63]
_ = x[actReplaceQuery-64] _ = x[actRefreshPreview-64]
_ = x[actToggleSort-65] _ = x[actReplaceQuery-65]
_ = x[actShowPreview-66] _ = x[actToggleSort-66]
_ = x[actHidePreview-67] _ = x[actShowPreview-67]
_ = x[actTogglePreview-68] _ = x[actHidePreview-68]
_ = x[actTogglePreviewWrap-69] _ = x[actTogglePreview-69]
_ = x[actTransform-70] _ = x[actTogglePreviewWrap-70]
_ = x[actTransformBorderLabel-71] _ = x[actTransform-71]
_ = x[actTransformHeader-72] _ = x[actTransformBorderLabel-72]
_ = x[actTransformPreviewLabel-73] _ = x[actTransformHeader-73]
_ = x[actTransformPrompt-74] _ = x[actTransformPreviewLabel-74]
_ = x[actTransformQuery-75] _ = x[actTransformPrompt-75]
_ = x[actPreview-76] _ = x[actTransformQuery-76]
_ = x[actChangePreview-77] _ = x[actPreview-77]
_ = x[actChangePreviewWindow-78] _ = x[actChangePreview-78]
_ = x[actPreviewTop-79] _ = x[actChangePreviewWindow-79]
_ = x[actPreviewBottom-80] _ = x[actPreviewTop-80]
_ = x[actPreviewUp-81] _ = x[actPreviewBottom-81]
_ = x[actPreviewDown-82] _ = x[actPreviewUp-82]
_ = x[actPreviewPageUp-83] _ = x[actPreviewDown-83]
_ = x[actPreviewPageDown-84] _ = x[actPreviewPageUp-84]
_ = x[actPreviewHalfPageUp-85] _ = x[actPreviewPageDown-85]
_ = x[actPreviewHalfPageDown-86] _ = x[actPreviewHalfPageUp-86]
_ = x[actPrevHistory-87] _ = x[actPreviewHalfPageDown-87]
_ = x[actPrevSelected-88] _ = x[actPrevHistory-88]
_ = x[actPut-89] _ = x[actPrevSelected-89]
_ = x[actNextHistory-90] _ = x[actPut-90]
_ = x[actNextSelected-91] _ = x[actNextHistory-91]
_ = x[actExecute-92] _ = x[actNextSelected-92]
_ = x[actExecuteSilent-93] _ = x[actExecute-93]
_ = x[actExecuteMulti-94] _ = x[actExecuteSilent-94]
_ = x[actSigStop-95] _ = x[actExecuteMulti-95]
_ = x[actFirst-96] _ = x[actSigStop-96]
_ = x[actLast-97] _ = x[actFirst-97]
_ = x[actReload-98] _ = x[actLast-98]
_ = x[actReloadSync-99] _ = x[actReload-99]
_ = x[actDisableSearch-100] _ = x[actReloadSync-100]
_ = x[actEnableSearch-101] _ = x[actDisableSearch-101]
_ = x[actSelect-102] _ = x[actEnableSearch-102]
_ = x[actDeselect-103] _ = x[actSelect-103]
_ = x[actUnbind-104] _ = x[actDeselect-104]
_ = x[actRebind-105] _ = x[actUnbind-105]
_ = x[actBecome-106] _ = x[actRebind-106]
_ = x[actResponse-107] _ = x[actBecome-107]
_ = x[actShowHeader-108] _ = x[actResponse-108]
_ = x[actHideHeader-109] _ = x[actShowHeader-109]
_ = x[actHideHeader-110]
} }
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader" const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 413, 427, 438, 449, 467, 484, 491, 510, 522, 536, 545, 560, 572, 585, 596, 607, 619, 633, 654, 669, 684, 701, 708, 713, 722, 733, 744, 757, 772, 783, 796, 803, 816, 829, 846, 861, 874, 888, 902, 918, 938, 950, 973, 991, 1015, 1033, 1050, 1060, 1076, 1098, 1111, 1127, 1139, 1153, 1169, 1187, 1207, 1229, 1243, 1258, 1264, 1278, 1293, 1303, 1319, 1334, 1344, 1352, 1359, 1368, 1381, 1397, 1412, 1421, 1432, 1441, 1450, 1459, 1470, 1483, 1496} var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 692, 709, 716, 721, 730, 741, 752, 765, 780, 791, 804, 811, 824, 837, 854, 869, 882, 896, 910, 926, 946, 958, 981, 999, 1023, 1041, 1058, 1068, 1084, 1106, 1119, 1135, 1147, 1161, 1177, 1195, 1215, 1237, 1251, 1266, 1272, 1286, 1301, 1311, 1327, 1342, 1352, 1360, 1367, 1376, 1389, 1405, 1420, 1429, 1440, 1449, 1458, 1467, 1478, 1491, 1504}
func (i actionType) String() string { func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) { if i < 0 || i >= actionType(len(_actionType_index)-1) {

@ -152,7 +152,7 @@ var (
// Extra bonus for word boundary after slash, colon, semi-colon, and comma // Extra bonus for word boundary after slash, colon, semi-colon, and comma
bonusBoundaryDelimiter int16 = bonusBoundary + 1 bonusBoundaryDelimiter int16 = bonusBoundary + 1
initialCharClass charClass = charWhite initialCharClass = charWhite
// A minor optimization that can give 15%+ performance boost // A minor optimization that can give 15%+ performance boost
asciiCharClasses [unicode.MaxASCII + 1]charClass asciiCharClasses [unicode.MaxASCII + 1]charClass

@ -3,7 +3,7 @@
package algo package algo
var normalized map[rune]rune = map[rune]rune{ var normalized = map[rune]rune{
0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER 0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER
0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER 0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER
0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER 0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER

@ -292,7 +292,7 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
func parseAnsiCode(s string, delimiter byte) (int, byte, string) { func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
var remaining string var remaining string
i := -1 var i int
if delimiter == 0 { if delimiter == 0 {
// Faster than strings.IndexAny(";:") // Faster than strings.IndexAny(";:")
i = strings.IndexByte(s, ';') i = strings.IndexByte(s, ';')
@ -350,7 +350,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state256 := 0 state256 := 0
ptr := &state.fg ptr := &state.fg
var delimiter byte = 0 var delimiter byte
count := 0 count := 0
for len(ansiCode) != 0 { for len(ansiCode) != 0 {
var num int var num int

@ -12,8 +12,14 @@ type ChunkCache struct {
} }
// NewChunkCache returns a new ChunkCache // NewChunkCache returns a new ChunkCache
func NewChunkCache() ChunkCache { func NewChunkCache() *ChunkCache {
return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)} return &ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
}
func (cc *ChunkCache) Clear() {
cc.mutex.Lock()
cc.cache = make(map[*Chunk]*queryCache)
cc.mutex.Unlock()
} }
// Add adds the list to the cache // Add adds the list to the cache

@ -67,9 +67,9 @@ const (
) )
const ( const (
exitCancel = -1 ExitCancel = -1
exitOk = 0 ExitOk = 0
exitNoMatch = 1 ExitNoMatch = 1
exitError = 2 ExitError = 2
exitInterrupt = 130 ExitInterrupt = 130
) )

@ -2,7 +2,6 @@
package fzf package fzf
import ( import (
"fmt"
"sync" "sync"
"time" "time"
"unsafe" "unsafe"
@ -27,22 +26,29 @@ func sbytes(data string) []byte {
return unsafe.Slice(unsafe.StringData(data), len(data)) return unsafe.Slice(unsafe.StringData(data), len(data))
} }
type quitSignal struct {
code int
err error
}
// Run starts fzf // Run starts fzf
func Run(opts *Options, version string, revision string) { func Run(opts *Options) (int, error) {
defer util.RunAtExitFuncs() if err := postProcessOptions(opts); err != nil {
return ExitError, err
}
sort := opts.Sort > 0 defer util.RunAtExitFuncs()
sortCriteria = opts.Criteria
if opts.Version { // Output channel given
if len(revision) > 0 { if opts.Output != nil {
fmt.Printf("%s (%s)\n", version, revision) opts.Printer = func(str string) {
} else { opts.Output <- str
fmt.Println(version)
} }
util.Exit(exitOk)
} }
sort := opts.Sort > 0
sortCriteria = opts.Criteria
// Event channel // Event channel
eventBox := util.NewEventBox() eventBox := util.NewEventBox()
@ -131,7 +137,7 @@ func Run(opts *Options, version string, revision string) {
reader = NewReader(func(data []byte) bool { reader = NewReader(func(data []byte) bool {
return chunkList.Push(data) return chunkList.Push(data)
}, eventBox, executor, opts.ReadZero, opts.Filter == nil) }, eventBox, executor, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip) go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} }
// Matcher // Matcher
@ -147,14 +153,16 @@ func Run(opts *Options, version string, revision string) {
forward = true forward = true
} }
} }
cache := NewChunkCache()
patternCache := make(map[string]*Pattern)
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern( return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos, opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, opts.Nth, opts.Delimiter, runes) opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
} }
inputRevision := 0 inputRevision := 0
snapshotRevision := 0 snapshotRevision := 0
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox, inputRevision) matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
// Filtering mode // Filtering mode
if opts.Filter != nil { if opts.Filter != nil {
@ -182,7 +190,7 @@ func Run(opts *Options, version string, revision string) {
} }
return false return false
}, eventBox, executor, opts.ReadZero, false) }, eventBox, executor, opts.ReadZero, false)
reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip) reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} else { } else {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin) eventBox.WaitFor(EvtReadFin)
@ -197,9 +205,9 @@ func Run(opts *Options, version string, revision string) {
} }
} }
if found { if found {
util.Exit(exitOk) return ExitOk, nil
} }
util.Exit(exitNoMatch) return ExitNoMatch, nil
} }
// Synchronous search // Synchronous search
@ -210,9 +218,13 @@ func Run(opts *Options, version string, revision string) {
// Go interactive // Go interactive
go matcher.Loop() go matcher.Loop()
defer matcher.Stop()
// Terminal I/O // Terminal I/O
terminal := NewTerminal(opts, eventBox, executor) terminal, err := NewTerminal(opts, eventBox, executor)
if err != nil {
return ExitError, err
}
maxFit := 0 // Maximum number of items that can fit on screen maxFit := 0 // Maximum number of items that can fit on screen
padHeight := 0 padHeight := 0
heightUnknown := opts.Height.auto heightUnknown := opts.Height.auto
@ -258,6 +270,9 @@ func Run(opts *Options, version string, revision string) {
header = make([]string, 0, opts.HeaderLines) header = make([]string, 0, opts.HeaderLines)
go reader.restart(command, environ) go reader.restart(command, environ)
} }
exitCode := ExitOk
stop := false
for { for {
delay := true delay := true
ticks++ ticks++
@ -278,7 +293,11 @@ func Run(opts *Options, version string, revision string) {
if reading { if reading {
reader.terminate() reader.terminate()
} }
util.Exit(value.(int)) quitSignal := value.(quitSignal)
exitCode = quitSignal.code
err = quitSignal.err
stop = true
return
case EvtReadNew, EvtReadFin: case EvtReadNew, EvtReadFin:
if evt == EvtReadFin && nextCommand != nil { if evt == EvtReadFin && nextCommand != nil {
restart(*nextCommand, nextEnviron) restart(*nextCommand, nextEnviron)
@ -378,10 +397,11 @@ func Run(opts *Options, version string, revision string) {
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
opts.Printer(val.Get(i).item.AsString(opts.Ansi)) opts.Printer(val.Get(i).item.AsString(opts.Ansi))
} }
if count > 0 { if count == 0 {
util.Exit(exitOk) exitCode = ExitNoMatch
} }
util.Exit(exitNoMatch) stop = true
return
} }
determine(val.final) determine(val.final)
} }
@ -392,6 +412,9 @@ func Run(opts *Options, version string, revision string) {
} }
events.Clear() events.Clear()
}) })
if stop {
break
}
if delay && reading { if delay && reading {
dur := util.DurWithin( dur := util.DurWithin(
time.Duration(ticks)*coordinatorDelayStep, time.Duration(ticks)*coordinatorDelayStep,
@ -399,4 +422,5 @@ func Run(opts *Options, version string, revision string) {
time.Sleep(dur) time.Sleep(dur)
} }
} }
return exitCode, err
} }

@ -21,6 +21,7 @@ type MatchRequest struct {
// Matcher is responsible for performing search // Matcher is responsible for performing search
type Matcher struct { type Matcher struct {
cache *ChunkCache
patternBuilder func([]rune) *Pattern patternBuilder func([]rune) *Pattern
sort bool sort bool
tac bool tac bool
@ -38,10 +39,11 @@ const (
) )
// NewMatcher returns a new Matcher // NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern, func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher { sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher {
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions) partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
return &Matcher{ return &Matcher{
cache: cache,
patternBuilder: patternBuilder, patternBuilder: patternBuilder,
sort: sort, sort: sort,
tac: tac, tac: tac,
@ -60,8 +62,13 @@ func (m *Matcher) Loop() {
for { for {
var request MatchRequest var request MatchRequest
stop := false
m.reqBox.Wait(func(events *util.Events) { m.reqBox.Wait(func(events *util.Events) {
for _, val := range *events { for t, val := range *events {
if t == reqQuit {
stop = true
return
}
switch val := val.(type) { switch val := val.(type) {
case MatchRequest: case MatchRequest:
request = val request = val
@ -71,12 +78,15 @@ func (m *Matcher) Loop() {
} }
events.Clear() events.Clear()
}) })
if stop {
break
}
if request.sort != m.sort || request.revision != m.revision { if request.sort != m.sort || request.revision != m.revision {
m.sort = request.sort m.sort = request.sort
m.revision = request.revision m.revision = request.revision
m.mergerCache = make(map[string]*Merger) m.mergerCache = make(map[string]*Merger)
clearChunkCache() m.cache.Clear()
} }
// Restart search // Restart search
@ -236,3 +246,7 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} }
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision}) m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
} }
func (m *Matcher) Stop() {
m.reqBox.Set(reqQuit, nil)
}

File diff suppressed because it is too large Load Diff

@ -3,9 +3,11 @@
package fzf package fzf
import "errors"
func (o *Options) initProfiling() error { func (o *Options) initProfiling() error {
if o.CPUProfile != "" || o.MEMProfile != "" || o.BlockProfile != "" || o.MutexProfile != "" { if o.CPUProfile != "" || o.MEMProfile != "" || o.BlockProfile != "" || o.MutexProfile != "" {
errorExit("error: profiling not supported: FZF must be built with '-tags=pprof' to enable profiling") return errors.New("error: profiling not supported: FZF must be built with '-tags=pprof' to enable profiling")
} }
return nil return nil
} }

@ -80,7 +80,7 @@ func TestDelimiterRegexRegexCaret(t *testing.T) {
func TestSplitNth(t *testing.T) { func TestSplitNth(t *testing.T) {
{ {
ranges := splitNth("..") ranges, _ := splitNth("..")
if len(ranges) != 1 || if len(ranges) != 1 ||
ranges[0].begin != rangeEllipsis || ranges[0].begin != rangeEllipsis ||
ranges[0].end != rangeEllipsis { ranges[0].end != rangeEllipsis {
@ -88,7 +88,7 @@ func TestSplitNth(t *testing.T) {
} }
} }
{ {
ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1") ranges, _ := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
if len(ranges) != 10 || if len(ranges) != 10 ||
ranges[0].begin != rangeEllipsis || ranges[0].end != 3 || ranges[0].begin != rangeEllipsis || ranges[0].end != 3 ||
ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis || ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis ||
@ -137,7 +137,7 @@ func TestIrrelevantNth(t *testing.T) {
} }
func TestParseKeys(t *testing.T) { func TestParseKeys(t *testing.T) {
pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "") pairs, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
checkEvent := func(e tui.Event, s string) { checkEvent := func(e tui.Event, s string) {
if pairs[e] != s { if pairs[e] != s {
t.Errorf("%s != %s", pairs[e], s) t.Errorf("%s != %s", pairs[e], s)
@ -163,7 +163,7 @@ func TestParseKeys(t *testing.T) {
checkEvent(tui.AltKey(' '), "alt-SPACE") checkEvent(tui.AltKey(' '), "alt-SPACE")
// Synonyms // Synonyms
pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") pairs, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
if len(pairs) != 9 { if len(pairs) != 9 {
t.Error(9) t.Error(9)
} }
@ -177,7 +177,7 @@ func TestParseKeys(t *testing.T) {
check(tui.Left, "left") check(tui.Left, "left")
check(tui.Right, "right") check(tui.Right, "right")
pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "") pairs, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
if len(pairs) != 11 { if len(pairs) != 11 {
t.Error(11) t.Error(11)
} }
@ -206,40 +206,40 @@ func TestParseKeysWithComma(t *testing.T) {
} }
} }
pairs := parseKeyChords(",", "") pairs, _ := parseKeyChords(",", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",,a,b", "") pairs, _ = parseKeyChords(",,a,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,b,,", "") pairs, _ = parseKeyChords("a,b,,", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,,,b", "") pairs, _ = parseKeyChords("a,,,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,,,b,c", "") pairs, _ = parseKeyChords("a,,,b,c", "")
checkN(len(pairs), 4) checkN(len(pairs), 4)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key('c'), "c") check(pairs, tui.Key('c'), "c")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",,,", "") pairs, _ = parseKeyChords(",,,", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",ALT-,,", "") pairs, _ = parseKeyChords(",ALT-,,", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, tui.AltKey(','), "ALT-,") check(pairs, tui.AltKey(','), "ALT-,")
} }
@ -262,17 +262,13 @@ func TestBind(t *testing.T) {
} }
} }
check(tui.CtrlA.AsEvent(), "", actBeginningOfLine) check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
errorString := ""
errorFn := func(e string) {
errorString = e
}
parseKeymap(keymap, parseKeymap(keymap,
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+ "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
"f1:execute(ls {+})+abort+execute(echo \n{+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "f1:execute(ls {+})+abort+execute(echo \n{+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+ "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
"x:Execute(foo+bar),X:execute/bar+baz/"+ "x:Execute(foo+bar),X:execute/bar+baz/"+
",f1:+first,f1:+top"+ ",f1:+first,f1:+top"+
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up", errorFn) ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
check(tui.CtrlA.AsEvent(), "", actKillLine) check(tui.CtrlA.AsEvent(), "", actKillLine)
check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown) check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown)
check(tui.Key('c'), "", actPageUp) check(tui.Key('c'), "", actPageUp)
@ -290,20 +286,17 @@ func TestBind(t *testing.T) {
check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute) check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute)
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char), errorFn) parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute) check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
} }
parseKeymap(keymap, "f1:abort", errorFn) parseKeymap(keymap, "f1:abort")
check(tui.F1.AsEvent(), "", actAbort) check(tui.F1.AsEvent(), "", actAbort)
if len(errorString) > 0 {
t.Errorf("error parsing keymap: %s", errorString)
}
} }
func TestColorSpec(t *testing.T) { func TestColorSpec(t *testing.T) {
theme := tui.Dark256 theme := tui.Dark256
dark := parseTheme(theme, "dark") dark, _ := parseTheme(theme, "dark")
if *dark != *theme { if *dark != *theme {
t.Errorf("colors should be equivalent") t.Errorf("colors should be equivalent")
} }
@ -311,7 +304,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("point should not be equivalent") t.Errorf("point should not be equivalent")
} }
light := parseTheme(theme, "dark,light") light, _ := parseTheme(theme, "dark,light")
if *light == *theme { if *light == *theme {
t.Errorf("should not be equivalent") t.Errorf("should not be equivalent")
} }
@ -322,7 +315,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("point should not be equivalent") t.Errorf("point should not be equivalent")
} }
customized := parseTheme(theme, "fg:231,bg:232") customized, _ := parseTheme(theme, "fg:231,bg:232")
if customized.Fg.Color != 231 || customized.Bg.Color != 232 { if customized.Fg.Color != 231 || customized.Bg.Color != 232 {
t.Errorf("color not customized") t.Errorf("color not customized")
} }
@ -335,7 +328,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized) t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
} }
customized = parseTheme(theme, "fg:231,dark,bg:232") customized, _ = parseTheme(theme, "fg:231,dark,bg:232")
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg { if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
t.Errorf("color not customized") t.Errorf("color not customized")
} }
@ -475,7 +468,7 @@ func TestValidateSign(t *testing.T) {
} }
func TestParseSingleActionList(t *testing.T) { func TestParseSingleActionList(t *testing.T) {
actions := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down", func(string) {}) actions, _ := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down")
if len(actions) != 4 { if len(actions) != 4 {
t.Errorf("Invalid number of actions parsed:%d", len(actions)) t.Errorf("Invalid number of actions parsed:%d", len(actions))
} }
@ -491,11 +484,8 @@ func TestParseSingleActionList(t *testing.T) {
} }
func TestParseSingleActionListError(t *testing.T) { func TestParseSingleActionListError(t *testing.T) {
err := "" _, err := parseSingleActionList("change-query(foobar)baz")
parseSingleActionList("change-query(foobar)baz", func(e string) { if err == nil {
err = e
})
if len(err) == 0 {
t.Errorf("Failed to detect error") t.Errorf("Failed to detect error")
} }
} }

@ -60,32 +60,17 @@ type Pattern struct {
delimiter Delimiter delimiter Delimiter
nth []Range nth []Range
procFun map[termType]algo.Algo procFun map[termType]algo.Algo
cache *ChunkCache
} }
var ( var _splitRegex *regexp.Regexp
_patternCache map[string]*Pattern
_splitRegex *regexp.Regexp
_cache ChunkCache
)
func init() { func init() {
_splitRegex = regexp.MustCompile(" +") _splitRegex = regexp.MustCompile(" +")
clearPatternCache()
clearChunkCache()
}
func clearPatternCache() {
// We can uniquely identify the pattern for a given string since
// search mode and caseMode do not change while the program is running
_patternCache = make(map[string]*Pattern)
}
func clearChunkCache() {
_cache = NewChunkCache()
} }
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool, func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string var asString string
@ -98,7 +83,9 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
asString = string(runes) asString = string(runes)
} }
cached, found := _patternCache[asString] // We can uniquely identify the pattern for a given string since
// search mode and caseMode do not change while the program is running
cached, found := patternCache[asString]
if found { if found {
return cached return cached
} }
@ -153,6 +140,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
cacheable: cacheable, cacheable: cacheable,
nth: nth, nth: nth,
delimiter: delimiter, delimiter: delimiter,
cache: cache,
procFun: make(map[termType]algo.Algo)} procFun: make(map[termType]algo.Algo)}
ptr.cacheKey = ptr.buildCacheKey() ptr.cacheKey = ptr.buildCacheKey()
@ -162,7 +150,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
ptr.procFun[termPrefix] = algo.PrefixMatch ptr.procFun[termPrefix] = algo.PrefixMatch
ptr.procFun[termSuffix] = algo.SuffixMatch ptr.procFun[termSuffix] = algo.SuffixMatch
_patternCache[asString] = ptr patternCache[asString] = ptr
return ptr return ptr
} }
@ -282,18 +270,18 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
// ChunkCache: Exact match // ChunkCache: Exact match
cacheKey := p.CacheKey() cacheKey := p.CacheKey()
if p.cacheable { if p.cacheable {
if cached := _cache.Lookup(chunk, cacheKey); cached != nil { if cached := p.cache.Lookup(chunk, cacheKey); cached != nil {
return cached return cached
} }
} }
// Prefix/suffix cache // Prefix/suffix cache
space := _cache.Search(chunk, cacheKey) space := p.cache.Search(chunk, cacheKey)
matches := p.matchChunk(chunk, space, slab) matches := p.matchChunk(chunk, space, slab)
if p.cacheable { if p.cacheable {
_cache.Add(chunk, cacheKey, matches) p.cache.Add(chunk, cacheKey, matches)
} }
return matches return matches
} }

@ -64,10 +64,15 @@ func TestParseTermsEmpty(t *testing.T) {
} }
} }
func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
withPos, cacheable, nth, delimiter, runes)
}
func TestExact(t *testing.T) { func TestExact(t *testing.T) {
defer clearPatternCache() pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
clearPatternCache()
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
[]Range{}, Delimiter{}, []rune("'abc")) []Range{}, Delimiter{}, []rune("'abc"))
chars := util.ToChars([]byte("aabbcc abc")) chars := util.ToChars([]byte("aabbcc abc"))
res, pos := algo.ExactMatchNaive( res, pos := algo.ExactMatchNaive(
@ -81,9 +86,7 @@ func TestExact(t *testing.T) {
} }
func TestEqual(t *testing.T) { func TestEqual(t *testing.T) {
defer clearPatternCache() pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
clearPatternCache()
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int, eidxExpected int) { match := func(str string, sidxExpected int, eidxExpected int) {
chars := util.ToChars([]byte(str)) chars := util.ToChars([]byte(str))
@ -104,19 +107,12 @@ func TestEqual(t *testing.T) {
} }
func TestCaseSensitivity(t *testing.T) { func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache() pat1 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() pat2 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc")) pat3 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() pat4 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc")) pat5 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() pat6 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache()
pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
if string(pat1.text) != "abc" || pat1.caseSensitive != false || if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true || string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@ -129,7 +125,7 @@ func TestCaseSensitivity(t *testing.T) {
} }
func TestOrigTextAndTransformed(t *testing.T) { func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg")) pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize("junegunn", Delimiter{}) tokens := Tokenize("junegunn", Delimiter{})
trans := Transform(tokens, []Range{{1, 1}}) trans := Transform(tokens, []Range{{1, 1}})
@ -163,15 +159,13 @@ func TestOrigTextAndTransformed(t *testing.T) {
func TestCacheKey(t *testing.T) { func TestCacheKey(t *testing.T) {
test := func(extended bool, patStr string, expected string, cacheable bool) { test := func(extended bool, patStr string, expected string, cacheable bool) {
clearPatternCache() pat := buildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
if pat.CacheKey() != expected { if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
} }
if pat.cacheable != cacheable { if pat.cacheable != cacheable {
t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr) t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr)
} }
clearPatternCache()
} }
test(false, "foo !bar", "foo !bar", true) test(false, "foo !bar", "foo !bar", true)
test(false, "foo | bar !baz", "foo | bar !baz", true) test(false, "foo | bar !baz", "foo | bar !baz", true)
@ -187,15 +181,13 @@ func TestCacheKey(t *testing.T) {
func TestCacheable(t *testing.T) { func TestCacheable(t *testing.T) {
test := func(fuzzy bool, str string, expected string, cacheable bool) { test := func(fuzzy bool, str string, expected string, cacheable bool) {
clearPatternCache() pat := buildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
if pat.CacheKey() != expected { if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
} }
if cacheable != pat.cacheable { if cacheable != pat.cacheable {
t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable) t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable)
} }
clearPatternCache()
} }
test(true, "foo bar", "foo\tbar", true) test(true, "foo bar", "foo\tbar", true)
test(true, "foo 'bar", "foo\tbar", false) test(true, "foo 'bar", "foo\tbar", false)

@ -3,6 +3,4 @@
package protector package protector
// Protect calls OS specific protections like pledge on OpenBSD // Protect calls OS specific protections like pledge on OpenBSD
func Protect() { func Protect() {}
return
}

@ -93,11 +93,26 @@ func (r *Reader) restart(command string, environ []string) {
r.fin(success) r.fin(success)
} }
func (r *Reader) readChannel(inputChan chan string) bool {
for {
item, more := <-inputChan
if !more {
break
}
if r.pusher([]byte(item)) {
atomic.StoreInt32(&r.event, int32(EvtReadNew))
}
}
return true
}
// ReadSource reads data from the default command or from standard input // ReadSource reads data from the default command or from standard input
func (r *Reader) ReadSource(root string, opts walkerOpts, ignores []string) { func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string) {
r.startEventPoller() r.startEventPoller()
var success bool var success bool
if util.IsTty() { if inputChan != nil {
success = r.readChannel(inputChan)
} else if util.IsTty() {
cmd := os.Getenv("FZF_DEFAULT_COMMAND") cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 { if len(cmd) == 0 {
success = r.readFiles(root, opts, ignores) success = r.readFiles(root, opts, ignores)

@ -73,28 +73,28 @@ func parseListenAddress(address string) (listenAddress, error) {
return listenAddress{parts[0], port}, nil return listenAddress{parts[0], port}, nil
} }
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (int, error) { func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (net.Listener, int, error) {
host := address.host host := address.host
port := address.port port := address.port
apiKey := os.Getenv("FZF_API_KEY") apiKey := os.Getenv("FZF_API_KEY")
if !address.IsLocal() && len(apiKey) == 0 { if !address.IsLocal() && len(apiKey) == 0 {
return port, errors.New("FZF_API_KEY is required to allow remote access") return nil, port, errors.New("FZF_API_KEY is required to allow remote access")
} }
addrStr := fmt.Sprintf("%s:%d", host, port) addrStr := fmt.Sprintf("%s:%d", host, port)
listener, err := net.Listen("tcp", addrStr) listener, err := net.Listen("tcp", addrStr)
if err != nil { if err != nil {
return port, fmt.Errorf("failed to listen on %s", addrStr) return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
} }
if port == 0 { if port == 0 {
addr := listener.Addr().String() addr := listener.Addr().String()
parts := strings.Split(addr, ":") parts := strings.Split(addr, ":")
if len(parts) < 2 { if len(parts) < 2 {
return port, fmt.Errorf("cannot extract port: %s", addr) return nil, port, fmt.Errorf("cannot extract port: %s", addr)
} }
var err error var err error
port, err = strconv.Atoi(parts[len(parts)-1]) port, err = strconv.Atoi(parts[len(parts)-1])
if err != nil { if err != nil {
return port, err return nil, port, err
} }
} }
@ -109,18 +109,16 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon
conn, err := listener.Accept() conn, err := listener.Accept()
if err != nil { if err != nil {
if errors.Is(err, net.ErrClosed) { if errors.Is(err, net.ErrClosed) {
break return
} else {
continue
} }
continue
} }
conn.Write([]byte(server.handleHttpRequest(conn))) conn.Write([]byte(server.handleHttpRequest(conn)))
conn.Close() conn.Close()
} }
listener.Close()
}() }()
return port, nil return listener, port, nil
} }
// Here we are writing a simplistic HTTP server without using net/http // Here we are writing a simplistic HTTP server without using net/http
@ -217,12 +215,9 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
} }
body = body[:contentLength] body = body[:contentLength]
errorMessage := "" actions, err := parseSingleActionList(strings.Trim(string(body), "\r\n"))
actions := parseSingleActionList(strings.Trim(string(body), "\r\n"), func(message string) { if err != nil {
errorMessage = message return bad(err.Error())
})
if len(errorMessage) > 0 {
return bad(errorMessage)
} }
if len(actions) == 0 { if len(actions) == 0 {
return bad("no action specified") return bad("no action specified")

@ -2,10 +2,12 @@ package fzf
import ( import (
"bufio" "bufio"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"math" "math"
"net"
"os" "os"
"os/signal" "os/signal"
"regexp" "regexp"
@ -49,8 +51,8 @@ var whiteSuffix *regexp.Regexp
var offsetComponentRegex *regexp.Regexp var offsetComponentRegex *regexp.Regexp
var offsetTrimCharsRegex *regexp.Regexp var offsetTrimCharsRegex *regexp.Regexp
var activeTempFiles []string var activeTempFiles []string
var activeTempFilesMutex sync.Mutex
var passThroughRegex *regexp.Regexp var passThroughRegex *regexp.Regexp
var actionTypeRegex *regexp.Regexp
const clearCode string = "\x1b[2J" const clearCode string = "\x1b[2J"
@ -63,6 +65,7 @@ func init() {
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`) offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`) offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
activeTempFiles = []string{} activeTempFiles = []string{}
activeTempFilesMutex = sync.Mutex{}
// Parts of the preview output that should be passed through to the terminal // 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 // * https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it
@ -241,6 +244,7 @@ type Terminal struct {
unicode bool unicode bool
listenAddr *listenAddress listenAddr *listenAddress
listenPort *int listenPort *int
listener net.Listener
listenUnsafe bool listenUnsafe bool
borderShape tui.BorderShape borderShape tui.BorderShape
cleanExit bool cleanExit bool
@ -259,7 +263,7 @@ type Terminal struct {
hasResizeActions bool hasResizeActions bool
triggerLoad bool triggerLoad bool
reading bool reading bool
running bool running *util.AtomicBool
failed *string failed *string
jumping jumpMode jumping jumpMode
jumpLabels string jumpLabels string
@ -278,12 +282,12 @@ type Terminal struct {
previewBox *util.EventBox previewBox *util.EventBox
eventBox *util.EventBox eventBox *util.EventBox
mutex sync.Mutex mutex sync.Mutex
initFunc func() initFunc func() error
prevLines []itemLine prevLines []itemLine
suppress bool suppress bool
sigstop bool sigstop bool
startChan chan fitpad startChan chan fitpad
killChan chan int killChan chan bool
serverInputChan chan []*action serverInputChan chan []*action
serverOutputChan chan string serverOutputChan chan string
eventChan chan tui.Event eventChan chan tui.Event
@ -340,6 +344,7 @@ const (
reqPreviewRefresh reqPreviewRefresh
reqPreviewDelayed reqPreviewDelayed
reqQuit reqQuit
reqFatal
) )
type action struct { type action struct {
@ -380,6 +385,7 @@ const (
actDeleteChar actDeleteChar
actDeleteCharEof actDeleteCharEof
actEndOfLine actEndOfLine
actFatal
actForwardChar actForwardChar
actForwardWord actForwardWord
actKillLine actKillLine
@ -511,6 +517,7 @@ type previewRequest struct {
pwindowSize tui.TermSize pwindowSize tui.TermSize
scrollOffset int scrollOffset int
list []*Item list []*Item
env []string
} }
type previewResult struct { type previewResult struct {
@ -537,6 +544,7 @@ func defaultKeymap() map[tui.Event][]*action {
keymap[e] = toActions(a) keymap[e] = toActions(a)
} }
add(tui.Fatal, actFatal)
add(tui.Invalid, actInvalid) add(tui.Invalid, actInvalid)
add(tui.CtrlA, actBeginningOfLine) add(tui.CtrlA, actBeginningOfLine)
add(tui.CtrlB, actBackwardChar) add(tui.CtrlB, actBackwardChar)
@ -642,7 +650,7 @@ func evaluateHeight(opts *Options, termHeight int) int {
} }
// NewTerminal returns new Terminal object // NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor) *Terminal { func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor) (*Terminal, error) {
input := trimQuery(opts.Query) input := trimQuery(opts.Query)
var delay time.Duration var delay time.Duration
if opts.Tac { if opts.Tac {
@ -660,11 +668,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
} }
var renderer tui.Renderer var renderer tui.Renderer
fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100) fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100)
var err error
if fullscreen { if fullscreen {
if tui.HasFullscreenRenderer() { if tui.HasFullscreenRenderer() {
renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
} else { } 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 }) true, func(h int) int { return h })
} }
} else { } else {
@ -680,7 +689,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
effectiveMinHeight += borderLines(opts.BorderShape) effectiveMinHeight += borderLines(opts.BorderShape)
return util.Min(termHeight, util.Max(evaluateHeight(opts, termHeight), effectiveMinHeight)) 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]" wordRubout := "[^\\pL\\pN][\\pL\\pN]"
wordNext := "[\\pL\\pN][^\\pL\\pN]|(.$)" wordNext := "[\\pL\\pN][^\\pL\\pN]|(.$)"
@ -693,6 +705,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
for key, action := range opts.Keymap { for key, action := range opts.Keymap {
keymapCopy[key] = action keymapCopy[key] = action
} }
t := Terminal{ t := Terminal{
initDelay: delay, initDelay: delay,
infoStyle: opts.InfoStyle, infoStyle: opts.InfoStyle,
@ -754,7 +767,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
hasLoadActions: false, hasLoadActions: false,
triggerLoad: false, triggerLoad: false,
reading: true, reading: true,
running: true, running: util.NewAtomicBool(true),
failed: nil, failed: nil,
jumping: jumpDisabled, jumping: jumpDisabled,
jumpLabels: opts.JumpLabels, jumpLabels: opts.JumpLabels,
@ -775,12 +788,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
slab: util.MakeSlab(slab16Size, slab32Size), slab: util.MakeSlab(slab16Size, slab32Size),
theme: opts.Theme, theme: opts.Theme,
startChan: make(chan fitpad, 1), startChan: make(chan fitpad, 1),
killChan: make(chan int), killChan: make(chan bool),
serverInputChan: make(chan []*action, 100), serverInputChan: make(chan []*action, 100),
serverOutputChan: make(chan string), serverOutputChan: make(chan string),
eventChan: make(chan tui.Event, 6), // (load + result + zero|one) | (focus) | (resize) | (GetChar) eventChan: make(chan tui.Event, 6), // (load + result + zero|one) | (focus) | (resize) | (GetChar)
tui: renderer, tui: renderer,
initFunc: func() { renderer.Init() }, initFunc: func() error { return renderer.Init() },
executing: util.NewAtomicBool(false), executing: util.NewAtomicBool(false),
lastAction: actStart, lastAction: actStart,
lastFocus: minItem.Index()} lastFocus: minItem.Index()}
@ -832,14 +845,15 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()] _, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
if t.listenAddr != nil { 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 { if err != nil {
errorExit(err.Error()) return nil, err
} }
t.listener = listener
t.listenPort = &port t.listenPort = &port
} }
return &t return &t, nil
} }
func (t *Terminal) environ() []string { func (t *Terminal) environ() []string {
@ -2103,12 +2117,11 @@ func (t *Terminal) renderPreviewArea(unchanged bool) {
} }
height := t.pwindow.Height() height := t.pwindow.Height()
header := []string{}
body := t.previewer.lines body := t.previewer.lines
headerLines := t.previewOpts.headerLines headerLines := t.previewOpts.headerLines
// Do not enable preview header lines if it's value is too large // Do not enable preview header lines if it's value is too large
if headerLines > 0 && headerLines < util.Min(len(body), height) { 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:] body = t.previewer.lines[headerLines:]
// Always redraw header // Always redraw header
t.renderPreviewText(height, header, 0, false) t.renderPreviewText(height, header, 0, false)
@ -2232,9 +2245,8 @@ Loop:
t.pwindow.Move(height-1, maxWidth-1) t.pwindow.Move(height-1, maxWidth-1)
t.previewed.filled = true t.previewed.filled = true
break Loop break Loop
} else {
t.pwindow.MoveAndClear(y+requiredLines, 0)
} }
t.pwindow.MoveAndClear(y+requiredLines, 0)
} }
} }
@ -2485,8 +2497,6 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
case 'q': case 'q':
flags.forceUpdate = true flags.forceUpdate = true
// query flag is not skipped // query flag is not skipped
default:
break
} }
} }
@ -2512,21 +2522,27 @@ func hasPreviewFlags(template string) (slot bool, plus bool, forceUpdate bool) {
func writeTemporaryFile(data []string, printSep string) string { func writeTemporaryFile(data []string, printSep string) string {
f, err := os.CreateTemp("", "fzf-preview-*") f, err := os.CreateTemp("", "fzf-preview-*")
if err != nil { if err != nil {
errorExit("Unable to create temporary file") // Unable to create temporary file
// FIXME: Should we terminate the program?
return ""
} }
defer f.Close() defer f.Close()
f.WriteString(strings.Join(data, printSep)) f.WriteString(strings.Join(data, printSep))
f.WriteString(printSep) f.WriteString(printSep)
activeTempFilesMutex.Lock()
activeTempFiles = append(activeTempFiles, f.Name()) activeTempFiles = append(activeTempFiles, f.Name())
activeTempFilesMutex.Unlock()
return f.Name() return f.Name()
} }
func cleanTemporaryFiles() { func cleanTemporaryFiles() {
activeTempFilesMutex.Lock()
for _, filename := range activeTempFiles { for _, filename := range activeTempFiles {
os.Remove(filename) os.Remove(filename)
} }
activeTempFiles = []string{} activeTempFiles = []string{}
activeTempFilesMutex.Unlock()
} }
type replacePlaceholderParams struct { type replacePlaceholderParams struct {
@ -2836,18 +2852,18 @@ func (t *Terminal) toggleItem(item *Item) bool {
return true return true
} }
func (t *Terminal) killPreview(code int) { func (t *Terminal) killPreview() {
select { select {
case t.killChan <- code: case t.killChan <- true:
default: default:
if code != exitCancel {
t.eventBox.Set(EvtQuit, code)
}
} }
} }
func (t *Terminal) cancelPreview() { func (t *Terminal) cancelPreview() {
t.killPreview(exitCancel) select {
case t.killChan <- false:
default:
}
} }
func (t *Terminal) pwindowSize() tui.TermSize { func (t *Terminal) pwindowSize() tui.TermSize {
@ -2871,7 +2887,7 @@ func (t *Terminal) currentIndex() int32 {
} }
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
func (t *Terminal) Loop() { func (t *Terminal) Loop() error {
// prof := profile.Start(profile.ProfilePath("/tmp/")) // prof := profile.Start(profile.ProfilePath("/tmp/"))
fitpad := <-t.startChan fitpad := <-t.startChan
fit := fitpad.fit fit := fitpad.fit
@ -2895,14 +2911,23 @@ func (t *Terminal) Loop() {
return util.Min(termHeight, contentHeight+pad) return util.Min(termHeight, contentHeight+pad)
}) })
} }
// Context
ctx, cancel := context.WithCancel(context.Background())
{ // Late initialization { // Late initialization
intChan := make(chan os.Signal, 1) intChan := make(chan os.Signal, 1)
signal.Notify(intChan, os.Interrupt, syscall.SIGTERM) signal.Notify(intChan, os.Interrupt, syscall.SIGTERM)
go func() { go func() {
for s := range intChan { for {
// Don't quit by SIGINT while executing because it should be for the executing command and not for fzf itself select {
if !(s == os.Interrupt && t.executing.Get()) { case <-ctx.Done():
t.reqBox.Set(reqQuit, nil) 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)
}
} }
} }
}() }()
@ -2911,8 +2936,12 @@ func (t *Terminal) Loop() {
notifyOnCont(contChan) notifyOnCont(contChan)
go func() { go func() {
for { for {
<-contChan select {
t.reqBox.Set(reqReinit, nil) case <-ctx.Done():
return
case <-contChan:
t.reqBox.Set(reqReinit, nil)
}
} }
}() }()
@ -2921,14 +2950,23 @@ func (t *Terminal) Loop() {
notifyOnResize(resizeChan) // Non-portable notifyOnResize(resizeChan) // Non-portable
go func() { go func() {
for { for {
<-resizeChan select {
t.reqBox.Set(reqResize, nil) case <-ctx.Done():
return
case <-resizeChan:
t.reqBox.Set(reqResize, nil)
}
} }
}() }()
} }
t.mutex.Lock() 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.termSize = t.tui.Size()
t.resizeWindows(false) t.resizeWindows(false)
t.window.Erase() t.window.Erase()
@ -2945,7 +2983,7 @@ func (t *Terminal) Loop() {
// Keep the spinner spinning // Keep the spinner spinning
go func() { go func() {
for { for t.running.Get() {
t.mutex.Lock() t.mutex.Lock()
reading := t.reading reading := t.reading
t.mutex.Unlock() t.mutex.Unlock()
@ -2960,15 +2998,20 @@ func (t *Terminal) Loop() {
if t.hasPreviewer() { if t.hasPreviewer() {
go func() { go func() {
var version int64 var version int64
stop := false
for { for {
var items []*Item var items []*Item
var commandTemplate string var commandTemplate string
var pwindow tui.Window var pwindow tui.Window
var pwindowSize tui.TermSize var pwindowSize tui.TermSize
var env []string
initialOffset := 0 initialOffset := 0
t.previewBox.Wait(func(events *util.Events) { t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events { for req, value := range *events {
switch req { switch req {
case reqQuit:
stop = true
return
case reqPreviewEnqueue: case reqPreviewEnqueue:
request := value.(previewRequest) request := value.(previewRequest)
commandTemplate = request.template commandTemplate = request.template
@ -2976,17 +3019,20 @@ func (t *Terminal) Loop() {
items = request.list items = request.list
pwindow = request.pwindow pwindow = request.pwindow
pwindowSize = request.pwindowSize pwindowSize = request.pwindowSize
env = request.env
} }
} }
events.Clear() events.Clear()
}) })
if stop {
break
}
version++ version++
// We don't display preview window if no match // We don't display preview window if no match
if items[0] != nil { if items[0] != nil {
_, query := t.Input() _, query := t.Input()
command := t.replacePlaceholder(commandTemplate, false, string(query), items) command := t.replacePlaceholder(commandTemplate, false, string(query), items)
cmd := t.executor.ExecCommand(command, true) cmd := t.executor.ExecCommand(command, true)
env := t.environ()
if pwindowSize.Lines > 0 { if pwindowSize.Lines > 0 {
lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines) lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines)
columns := fmt.Sprintf("COLUMNS=%d", pwindowSize.Columns) columns := fmt.Sprintf("COLUMNS=%d", pwindowSize.Columns)
@ -3071,12 +3117,13 @@ func (t *Terminal) Loop() {
Loop: Loop:
for { for {
select { select {
case <-ctx.Done():
break Loop
case <-timer.C: case <-timer.C:
t.reqBox.Set(reqPreviewDelayed, version) t.reqBox.Set(reqPreviewDelayed, version)
case code := <-t.killChan: case immediately := <-t.killChan:
if code != exitCancel { if immediately {
util.KillCommand(cmd) util.KillCommand(cmd)
t.eventBox.Set(EvtQuit, code)
} else { } else {
// We can immediately kill a long-running preview program // We can immediately kill a long-running preview program
// once we started rendering its partial output // once we started rendering its partial output
@ -3123,19 +3170,25 @@ func (t *Terminal) Loop() {
if len(command) > 0 && t.canPreview() { if len(command) > 0 && t.canPreview() {
_, list := t.buildPlusList(command, false) _, list := t.buildPlusList(command, false)
t.cancelPreview() 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() { go func() {
var focusedIndex int32 = minItem.Index() var focusedIndex = minItem.Index()
var version int64 = -1 var version int64 = -1
running := true running := true
code := exitError code := ExitError
exit := func(getCode func() int) { exit := func(getCode func() int) {
if t.hasPreviewer() {
t.previewBox.Set(reqQuit, nil)
}
if t.listener != nil {
t.listener.Close()
}
t.tui.Close() t.tui.Close()
code = getCode() code = getCode()
if code <= exitNoMatch && t.history != nil { if code <= ExitNoMatch && t.history != nil {
t.history.append(string(t.input)) t.history.append(string(t.input))
} }
running = false running = false
@ -3203,9 +3256,9 @@ func (t *Terminal) Loop() {
case reqClose: case reqClose:
exit(func() int { exit(func() int {
if t.output() { if t.output() {
return exitOk return ExitOk
} }
return exitNoMatch return ExitNoMatch
}) })
return return
case reqPreviewDisplay: case reqPreviewDisplay:
@ -3233,11 +3286,14 @@ func (t *Terminal) Loop() {
case reqPrintQuery: case reqPrintQuery:
exit(func() int { exit(func() int {
t.printer(string(t.input)) t.printer(string(t.input))
return exitOk return ExitOk
}) })
return return
case reqQuit: case reqQuit:
exit(func() int { return exitInterrupt }) exit(func() int { return ExitInterrupt })
return
case reqFatal:
exit(func() int { return ExitError })
return return
} }
} }
@ -3245,8 +3301,11 @@ func (t *Terminal) Loop() {
t.mutex.Unlock() t.mutex.Unlock()
}) })
} }
// prof.Stop()
t.killPreview(code) t.eventBox.Set(EvtQuit, quitSignal{code, nil})
t.running.Set(false)
t.killPreview()
cancel()
}() }()
looping := true looping := true
@ -3256,8 +3315,16 @@ func (t *Terminal) Loop() {
barrier := make(chan bool) barrier := make(chan bool)
go func() { go func() {
for { for {
<-barrier select {
t.eventChan <- t.tui.GetChar() case <-ctx.Done():
return
case <-barrier:
}
select {
case <-ctx.Done():
return
case t.eventChan <- t.tui.GetChar():
}
} }
}() }()
previewDraggingPos := -1 previewDraggingPos := -1
@ -3353,7 +3420,7 @@ func (t *Terminal) Loop() {
t.pressed = ret t.pressed = ret
t.reqBox.Set(reqClose, nil) t.reqBox.Set(reqClose, nil)
t.mutex.Unlock() t.mutex.Unlock()
return return nil
} }
} }
@ -3362,8 +3429,7 @@ func (t *Terminal) Loop() {
} }
var doAction func(*action) bool 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++ { for iter := 0; iter <= maxFocusEvents; iter++ {
currentIndex := t.currentIndex() currentIndex := t.currentIndex()
for _, action := range actions { for _, action := range actions {
@ -3433,7 +3499,7 @@ func (t *Terminal) Loop() {
if valid { if valid {
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, 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 { } else {
// Discard the preview content so that it won't accidentally appear // Discard the preview content so that it won't accidentally appear
@ -3547,8 +3613,9 @@ func (t *Terminal) Loop() {
} }
case actTransform: case actTransform:
body := t.executeCommand(a.a, false, true, true, false) body := t.executeCommand(a.a, false, true, true, false)
actions := parseSingleActionList(strings.Trim(body, "\r\n"), func(message string) {}) if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil {
return doActions(actions) return doActions(actions)
}
case actTransformBorderLabel: case actTransformBorderLabel:
label := t.executeCommand(a.a, false, true, true, true) label := t.executeCommand(a.a, false, true, true, true)
t.borderLabelOpts.label = label t.borderLabelOpts.label = label
@ -3580,6 +3647,8 @@ func (t *Terminal) Loop() {
t.input = current.text.ToRunes() t.input = current.text.ToRunes()
t.cx = len(t.input) t.cx = len(t.input)
} }
case actFatal:
req(reqFatal)
case actAbort: case actAbort:
req(reqQuit) req(reqQuit)
case actDeleteChar: case actDeleteChar:
@ -4005,10 +4074,10 @@ func (t *Terminal) Loop() {
} }
if me.Down { if me.Down {
mx_cons := util.Constrain(mx-t.promptLen, 0, len(t.input)) mxCons := util.Constrain(mx-t.promptLen, 0, len(t.input))
if my == t.promptLine() && mx_cons >= 0 { if my == t.promptLine() && mxCons >= 0 {
// Prompt // Prompt
t.cx = mx_cons + t.xoffset t.cx = mxCons + t.xoffset
} else if my >= min { } else if my >= min {
t.vset(t.offset + my - min) t.vset(t.offset + my - min)
req(reqList) req(reqList)
@ -4066,15 +4135,17 @@ func (t *Terminal) Loop() {
t.reading = true t.reading = true
} }
case actUnbind: case actUnbind:
keys := parseKeyChords(a.a, "PANIC") if keys, err := parseKeyChords(a.a, "PANIC"); err == nil {
for key := range keys { for key := range keys {
delete(t.keymap, key) delete(t.keymap, key)
}
} }
case actRebind: case actRebind:
keys := parseKeyChords(a.a, "PANIC") if keys, err := parseKeyChords(a.a, "PANIC"); err == nil {
for key := range keys { for key := range keys {
if originalAction, found := t.keymapOrg[key]; found { if originalAction, found := t.keymapOrg[key]; found {
t.keymap[key] = originalAction t.keymap[key] = originalAction
}
} }
} }
case actChangePreview: case actChangePreview:
@ -4221,6 +4292,7 @@ func (t *Terminal) Loop() {
t.reqBox.Set(event, nil) t.reqBox.Set(event, nil)
} }
} }
return nil
} }
func (t *Terminal) constrain() { func (t *Terminal) constrain() {

@ -71,14 +71,14 @@ func TestTransform(t *testing.T) {
{ {
tokens := Tokenize(input, Delimiter{}) tokens := Tokenize(input, Delimiter{})
{ {
ranges := splitNth("1,2,3") ranges, _ := splitNth("1,2,3")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if joinTokens(tx) != "abc: def: ghi: " { if joinTokens(tx) != "abc: def: ghi: " {
t.Errorf("%s", tx) t.Errorf("%s", tx)
} }
} }
{ {
ranges := splitNth("1..2,3,2..,1") ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " || if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
len(tx) != 4 || len(tx) != 4 ||
@ -93,7 +93,7 @@ func TestTransform(t *testing.T) {
{ {
tokens := Tokenize(input, delimiterRegexp(":")) tokens := Tokenize(input, delimiterRegexp(":"))
{ {
ranges := splitNth("1..2,3,2..,1") ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" || if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx) != 4 || len(tx) != 4 ||
@ -108,5 +108,6 @@ func TestTransform(t *testing.T) {
} }
func TestTransformIndexOutOfBounds(t *testing.T) { func TestTransformIndexOutOfBounds(t *testing.T) {
Transform([]Token{}, splitNth("1")) s, _ := splitNth("1")
Transform([]Token{}, s)
} }

@ -8,7 +8,7 @@ func HasFullscreenRenderer() bool {
return false return false
} }
var DefaultBorderShape BorderShape = BorderRounded var DefaultBorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr { func (a Attr) Merge(b Attr) Attr {
return a | b return a | b
@ -29,7 +29,7 @@ const (
StrikeThrough = Attr(1 << 7) StrikeThrough = Attr(1 << 7)
) )
func (r *FullscreenRenderer) Init() {} func (r *FullscreenRenderer) Init() error { return nil }
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {} func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) Pause(bool) {} func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Resume(bool, bool) {} func (r *FullscreenRenderer) Resume(bool, bool) {}

@ -83,34 +83,35 @@ func _() {
_ = x[Alt-72] _ = x[Alt-72]
_ = x[CtrlAlt-73] _ = x[CtrlAlt-73]
_ = x[Invalid-74] _ = x[Invalid-74]
_ = x[Mouse-75] _ = x[Fatal-75]
_ = x[DoubleClick-76] _ = x[Mouse-76]
_ = x[LeftClick-77] _ = x[DoubleClick-77]
_ = x[RightClick-78] _ = x[LeftClick-78]
_ = x[SLeftClick-79] _ = x[RightClick-79]
_ = x[SRightClick-80] _ = x[SLeftClick-80]
_ = x[ScrollUp-81] _ = x[SRightClick-81]
_ = x[ScrollDown-82] _ = x[ScrollUp-82]
_ = x[SScrollUp-83] _ = x[ScrollDown-83]
_ = x[SScrollDown-84] _ = x[SScrollUp-84]
_ = x[PreviewScrollUp-85] _ = x[SScrollDown-85]
_ = x[PreviewScrollDown-86] _ = x[PreviewScrollUp-86]
_ = x[Resize-87] _ = x[PreviewScrollDown-87]
_ = x[Change-88] _ = x[Resize-88]
_ = x[BackwardEOF-89] _ = x[Change-89]
_ = x[Start-90] _ = x[BackwardEOF-90]
_ = x[Load-91] _ = x[Start-91]
_ = x[Focus-92] _ = x[Load-92]
_ = x[One-93] _ = x[Focus-93]
_ = x[Zero-94] _ = x[One-94]
_ = x[Result-95] _ = x[Zero-95]
_ = x[Jump-96] _ = x[Result-96]
_ = x[JumpCancel-97] _ = x[Jump-97]
_ = x[JumpCancel-98]
} }
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel" const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel"
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 458, 467, 477, 487, 498, 506, 516, 525, 536, 551, 568, 574, 580, 591, 596, 600, 605, 608, 612, 618, 622, 632} var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637}
func (i EventType) String() string { func (i EventType) String() string {
if i < 0 || i >= EventType(len(_EventType_index)-1) { if i < 0 || i >= EventType(len(_EventType_index)-1) {

@ -2,6 +2,7 @@ package tui
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
@ -10,6 +11,7 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
"golang.org/x/term" "golang.org/x/term"
@ -27,8 +29,8 @@ const (
const consoleDevice string = "/dev/tty" const consoleDevice string = "/dev/tty"
var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R") var offsetRegexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R") var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
func (r *LightRenderer) PassThrough(str string) { func (r *LightRenderer) PassThrough(str string) {
r.queued.WriteString("\x1b7" + str + "\x1b8") r.queued.WriteString("\x1b7" + str + "\x1b8")
@ -78,6 +80,7 @@ func (r *LightRenderer) flush() {
// Light renderer // Light renderer
type LightRenderer struct { type LightRenderer struct {
closed *util.AtomicBool
theme *ColorTheme theme *ColorTheme
mouse bool mouse bool
forceBlack bool forceBlack bool
@ -123,19 +126,24 @@ type LightWindow struct {
bg Color bg Color
} }
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer { func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
in, err := openTtyIn()
if err != nil {
return nil, err
}
r := LightRenderer{ r := LightRenderer{
closed: util.NewAtomicBool(false),
theme: theme, theme: theme,
forceBlack: forceBlack, forceBlack: forceBlack,
mouse: mouse, mouse: mouse,
clearOnExit: clearOnExit, clearOnExit: clearOnExit,
ttyin: openTtyIn(), ttyin: in,
yoffset: 0, yoffset: 0,
tabstop: tabstop, tabstop: tabstop,
fullscreen: fullscreen, fullscreen: fullscreen,
upOneLine: false, upOneLine: false,
maxHeightFunc: maxHeightFunc} maxHeightFunc: maxHeightFunc}
return &r return &r, nil
} }
func repeat(r rune, times int) string { func repeat(r rune, times int) string {
@ -153,11 +161,11 @@ func atoi(s string, defaultValue int) int {
return value return value
} }
func (r *LightRenderer) Init() { func (r *LightRenderer) Init() error {
r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay) r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
if err := r.initPlatform(); err != nil { if err := r.initPlatform(); err != nil {
errorExit(err.Error()) return err
} }
r.updateTerminalSize() r.updateTerminalSize()
initTheme(r.theme, r.defaultTheme(), r.forceBlack) initTheme(r.theme, r.defaultTheme(), r.forceBlack)
@ -195,6 +203,7 @@ func (r *LightRenderer) Init() {
if !r.fullscreen && r.mouse { if !r.fullscreen && r.mouse {
r.yoffset, _ = r.findOffset() r.yoffset, _ = r.findOffset()
} }
return nil
} }
func (r *LightRenderer) Resize(maxHeightFunc func(int) int) { func (r *LightRenderer) Resize(maxHeightFunc func(int) int) {
@ -233,15 +242,16 @@ func getEnv(name string, defaultValue int) int {
return atoi(env, defaultValue) return atoi(env, defaultValue)
} }
func (r *LightRenderer) getBytes() []byte { func (r *LightRenderer) getBytes() ([]byte, error) {
return r.getBytesInternal(r.buffer, false) bytes, err := r.getBytesInternal(r.buffer, false)
return bytes, err
} }
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte { func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte, error) {
c, ok := r.getch(nonblock) c, ok := r.getch(nonblock)
if !nonblock && !ok { if !nonblock && !ok {
r.Close() r.Close()
errorExit("Failed to read " + consoleDevice) return nil, errors.New("failed to read " + consoleDevice)
} }
retries := 0 retries := 0
@ -272,19 +282,23 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
// so terminate fzf immediately. // so terminate fzf immediately.
if len(buffer) > maxInputBuffer { if len(buffer) > maxInputBuffer {
r.Close() r.Close()
panic(fmt.Sprintf("Input buffer overflow (%d): %v", len(buffer), buffer)) return nil, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer)
} }
} }
return buffer return buffer, nil
} }
func (r *LightRenderer) GetChar() Event { func (r *LightRenderer) GetChar() Event {
var err error
if len(r.buffer) == 0 { if len(r.buffer) == 0 {
r.buffer = r.getBytes() r.buffer, err = r.getBytes()
if err != nil {
return Event{Fatal, 0, nil}
}
} }
if len(r.buffer) == 0 { if len(r.buffer) == 0 {
panic("Empty buffer") return Event{Fatal, 0, nil}
} }
sz := 1 sz := 1
@ -315,7 +329,9 @@ func (r *LightRenderer) GetChar() Event {
ev := r.escSequence(&sz) ev := r.escSequence(&sz)
// Second chance // Second chance
if ev.Type == Invalid { if ev.Type == Invalid {
r.buffer = r.getBytes() if r.buffer, err = r.getBytes(); err != nil {
return Event{Fatal, 0, nil}
}
ev = r.escSequence(&sz) ev = r.escSequence(&sz)
} }
return ev return ev
@ -738,6 +754,7 @@ func (r *LightRenderer) Close() {
r.flush() r.flush()
r.closePlatform() r.closePlatform()
r.restoreTerminal() r.restoreTerminal()
r.closed.Set(true)
} }
func (r *LightRenderer) Top() int { func (r *LightRenderer) Top() int {

@ -3,7 +3,7 @@
package tui package tui
import ( import (
"fmt" "errors"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
@ -48,19 +48,18 @@ func (r *LightRenderer) closePlatform() {
// NOOP // NOOP
} }
func openTtyIn() *os.File { func openTtyIn() (*os.File, error) {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
if err != nil { if err != nil {
tty := ttyname() tty := ttyname()
if len(tty) > 0 { if len(tty) > 0 {
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil { if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
return in return in, nil
} }
} }
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice) return nil, errors.New("failed to open " + consoleDevice)
util.Exit(2)
} }
return in return in, nil
} }
func (r *LightRenderer) setupTerminal() { func (r *LightRenderer) setupTerminal() {
@ -86,9 +85,14 @@ func (r *LightRenderer) updateTerminalSize() {
func (r *LightRenderer) findOffset() (row int, col int) { func (r *LightRenderer) findOffset() (row int, col int) {
r.csi("6n") r.csi("6n")
r.flush() r.flush()
var err error
bytes := []byte{} bytes := []byte{}
for tries := 0; tries < offsetPollTries; tries++ { for tries := 0; tries < offsetPollTries; tries++ {
bytes = r.getBytesInternal(bytes, tries > 0) bytes, err = r.getBytesInternal(bytes, tries > 0)
if err != nil {
return -1, -1
}
offsets := offsetRegexp.FindSubmatch(bytes) offsets := offsetRegexp.FindSubmatch(bytes)
if len(offsets) > 3 { if len(offsets) > 3 {
// Add anything we skipped over to the input buffer // Add anything we skipped over to the input buffer

@ -72,7 +72,7 @@ func (r *LightRenderer) initPlatform() error {
go func() { go func() {
fd := int(r.inHandle) fd := int(r.inHandle)
b := make([]byte, 1) b := make([]byte, 1)
for { for !r.closed.Get() {
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT. // HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput) _ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
@ -91,9 +91,9 @@ func (r *LightRenderer) closePlatform() {
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput) windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
} }
func openTtyIn() *os.File { func openTtyIn() (*os.File, error) {
// not used // not used
return nil return nil, nil
} }
func (r *LightRenderer) setupTerminal() error { func (r *LightRenderer) setupTerminal() error {

@ -7,7 +7,6 @@ import (
"time" "time"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/encoding"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
@ -146,13 +145,13 @@ var (
_initialResize bool = true _initialResize bool = true
) )
func (r *FullscreenRenderer) initScreen() { func (r *FullscreenRenderer) initScreen() error {
s, e := tcell.NewScreen() s, e := tcell.NewScreen()
if e != nil { if e != nil {
errorExit(e.Error()) return e
} }
if e = s.Init(); e != nil { if e = s.Init(); e != nil {
errorExit(e.Error()) return e
} }
if r.mouse { if r.mouse {
s.EnableMouse() s.EnableMouse()
@ -160,16 +159,21 @@ func (r *FullscreenRenderer) initScreen() {
s.DisableMouse() s.DisableMouse()
} }
_screen = s _screen = s
return nil
} }
func (r *FullscreenRenderer) Init() { func (r *FullscreenRenderer) Init() error {
if os.Getenv("TERM") == "cygwin" { if os.Getenv("TERM") == "cygwin" {
os.Setenv("TERM", "") os.Setenv("TERM", "")
} }
encoding.Register()
r.initScreen() if err := r.initScreen(); err != nil {
return err
}
initTheme(r.theme, r.defaultTheme(), r.forceBlack) initTheme(r.theme, r.defaultTheme(), r.forceBlack)
return nil
} }
func (r *FullscreenRenderer) Top() int { func (r *FullscreenRenderer) Top() int {

@ -1,8 +1,6 @@
package tui package tui
import ( import (
"fmt"
"os"
"strconv" "strconv"
"time" "time"
@ -104,6 +102,7 @@ const (
CtrlAlt CtrlAlt
Invalid Invalid
Fatal
Mouse Mouse
DoubleClick DoubleClick
@ -525,7 +524,7 @@ type TermSize struct {
} }
type Renderer interface { type Renderer interface {
Init() Init() error
Resize(maxHeightFunc func(int) int) Resize(maxHeightFunc func(int) int)
Pause(clear bool) Pause(clear bool)
Resume(clear bool, sigcont bool) Resume(clear bool, sigcont bool)
@ -685,11 +684,6 @@ func NoColorTheme() *ColorTheme {
} }
} }
func errorExit(message string) {
fmt.Fprintln(os.Stderr, message)
util.Exit(2)
}
func init() { func init() {
Default16 = &ColorTheme{ Default16 = &ColorTheme{
Colored: true, Colored: true,

@ -1,7 +1,6 @@
package util package util
import ( import (
"os"
"sync" "sync"
) )
@ -25,14 +24,5 @@ func RunAtExitFuncs() {
for i := len(fns) - 1; i >= 0; i-- { for i := len(fns) - 1; i >= 0; i-- {
fns[i]() fns[i]()
} }
} atExitFuncs = nil
// Exit executes any functions registered with AtExit() then exits the program
// with os.Exit(code).
//
// NOTE: It must be used instead of os.Exit() since calling os.Exit() terminates
// the program before any of the AtExit functions can run.
func Exit(code int) {
defer os.Exit(code)
RunAtExitFuncs()
} }

@ -61,7 +61,7 @@ func (x *Executor) Become(stdin *os.File, environ []string, command string) {
shellPath, err := exec.LookPath(x.shell) shellPath, err := exec.LookPath(x.shell)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error()) fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
Exit(127) os.Exit(127)
} }
args := append([]string{shellPath}, append(x.args, command)...) args := append([]string{shellPath}, append(x.args, command)...)
SetStdin(stdin) SetStdin(stdin)

@ -97,15 +97,15 @@ func (x *Executor) Become(stdin *os.File, environ []string, command string) {
err := cmd.Start() err := cmd.Start()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error()) fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
Exit(127) os.Exit(127)
} }
err = cmd.Wait() err = cmd.Wait()
if err != nil { if err != nil {
if exitError, ok := err.(*exec.ExitError); ok { if exitError, ok := err.(*exec.ExitError); ok {
Exit(exitError.ExitCode()) os.Exit(exitError.ExitCode())
} }
} }
Exit(0) os.Exit(0)
} }
func escapeArg(s string) string { func escapeArg(s string) string {

@ -557,7 +557,7 @@ class TestGoFZF < TestBase
def test_expect def test_expect
test = lambda do |key, feed, expected = key| test = lambda do |key, feed, expected = key|
tmux.send_keys "seq 1 100 | #{fzf(:expect, key)}", :Enter tmux.send_keys "seq 1 100 | #{fzf(:expect, key, :prompt, "[#{key}]")}", :Enter
tmux.until { |lines| assert_equal ' 100/100', lines[-2] } tmux.until { |lines| assert_equal ' 100/100', lines[-2] }
tmux.send_keys '55' tmux.send_keys '55'
tmux.until { |lines| assert_equal ' 1/100', lines[-2] } tmux.until { |lines| assert_equal ' 1/100', lines[-2] }

Loading…
Cancel
Save