From e8405f40fe2eb3675f1cb4f69e825eff5f13f269 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 7 May 2024 01:06:42 +0900 Subject: [PATCH] Refactor the code so that fzf can be used as a library (#3769) --- Makefile | 1 - main.go | 35 +- main_test.go | 174 ------- src/actiontype_string.go | 167 ++++--- src/algo/algo.go | 2 +- src/algo/normalize.go | 2 +- src/ansi.go | 4 +- src/cache.go | 10 +- src/constants.go | 10 +- src/core.go | 68 ++- src/matcher.go | 20 +- src/options.go | 969 +++++++++++++++++++++++------------- src/options_no_pprof.go | 4 +- src/options_test.go | 54 +- src/pattern.go | 34 +- src/pattern_test.go | 44 +- src/protector/protector.go | 4 +- src/reader.go | 19 +- src/server.go | 27 +- src/terminal.go | 210 +++++--- src/tokenizer_test.go | 9 +- src/tui/dummy.go | 4 +- src/tui/eventtype_string.go | 51 +- src/tui/light.go | 49 +- src/tui/light_unix.go | 18 +- src/tui/light_windows.go | 6 +- src/tui/tcell.go | 18 +- src/tui/tui.go | 10 +- src/util/atexit.go | 12 +- src/util/util_unix.go | 2 +- src/util/util_windows.go | 6 +- test/test_go.rb | 2 +- 32 files changed, 1152 insertions(+), 893 deletions(-) delete mode 100644 main_test.go diff --git a/Makefile b/Makefile index 16150e5..f4abe6e 100644 --- a/Makefile +++ b/Makefile @@ -79,7 +79,6 @@ all: target/$(BINARY) test: $(SOURCES) [ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1) SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \ - github.com/junegunn/fzf \ github.com/junegunn/fzf/src \ github.com/junegunn/fzf/src/algo \ github.com/junegunn/fzf/src/tui \ diff --git a/main.go b/main.go index 768148e..f0074b3 100644 --- a/main.go +++ b/main.go @@ -3,14 +3,15 @@ package main import ( _ "embed" "fmt" + "os" "strings" fzf "github.com/junegunn/fzf/src" "github.com/junegunn/fzf/src/protector" ) -var version string = "0.51" -var revision string = "devel" +var version = "0.51" +var revision = "devel" //go:embed shell/key-bindings.bash var bashKeyBindings []byte @@ -33,9 +34,21 @@ func printScript(label string, content []byte) { fmt.Println("### end: " + label + " ###") } +func exit(code int, err error) { + if err != nil { + os.Stderr.WriteString(err.Error() + "\n") + } + os.Exit(code) +} + func main() { protector.Protect() - options := fzf.ParseOptions() + + options, err := fzf.ParseOptions(true, os.Args[1:]) + if err != nil { + exit(fzf.ExitError, err) + return + } if options.Bash { printScript("key-bindings.bash", bashKeyBindings) printScript("completion.bash", bashCompletion) @@ -51,5 +64,19 @@ func main() { fmt.Println("fzf_key_bindings") 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) } diff --git a/main_test.go b/main_test.go deleted file mode 100644 index 763b282..0000000 --- a/main_test.go +++ /dev/null @@ -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 ")) - } -} diff --git a/src/actiontype_string.go b/src/actiontype_string.go index a9d931d..6b07134 100644 --- a/src/actiontype_string.go +++ b/src/actiontype_string.go @@ -37,92 +37,93 @@ func _() { _ = x[actDeleteChar-26] _ = x[actDeleteCharEof-27] _ = x[actEndOfLine-28] - _ = x[actForwardChar-29] - _ = x[actForwardWord-30] - _ = x[actKillLine-31] - _ = x[actKillWord-32] - _ = x[actUnixLineDiscard-33] - _ = x[actUnixWordRubout-34] - _ = x[actYank-35] - _ = x[actBackwardKillWord-36] - _ = x[actSelectAll-37] - _ = x[actDeselectAll-38] - _ = x[actToggle-39] - _ = x[actToggleSearch-40] - _ = x[actToggleAll-41] - _ = x[actToggleDown-42] - _ = x[actToggleUp-43] - _ = x[actToggleIn-44] - _ = x[actToggleOut-45] - _ = x[actToggleTrack-46] - _ = x[actToggleTrackCurrent-47] - _ = x[actToggleHeader-48] - _ = x[actTrackCurrent-49] - _ = x[actUntrackCurrent-50] - _ = x[actDown-51] - _ = x[actUp-52] - _ = x[actPageUp-53] - _ = x[actPageDown-54] - _ = x[actPosition-55] - _ = x[actHalfPageUp-56] - _ = x[actHalfPageDown-57] - _ = x[actOffsetUp-58] - _ = x[actOffsetDown-59] - _ = x[actJump-60] - _ = x[actJumpAccept-61] - _ = x[actPrintQuery-62] - _ = x[actRefreshPreview-63] - _ = x[actReplaceQuery-64] - _ = x[actToggleSort-65] - _ = x[actShowPreview-66] - _ = x[actHidePreview-67] - _ = x[actTogglePreview-68] - _ = x[actTogglePreviewWrap-69] - _ = x[actTransform-70] - _ = x[actTransformBorderLabel-71] - _ = x[actTransformHeader-72] - _ = x[actTransformPreviewLabel-73] - _ = x[actTransformPrompt-74] - _ = x[actTransformQuery-75] - _ = x[actPreview-76] - _ = x[actChangePreview-77] - _ = x[actChangePreviewWindow-78] - _ = x[actPreviewTop-79] - _ = x[actPreviewBottom-80] - _ = x[actPreviewUp-81] - _ = x[actPreviewDown-82] - _ = x[actPreviewPageUp-83] - _ = x[actPreviewPageDown-84] - _ = x[actPreviewHalfPageUp-85] - _ = x[actPreviewHalfPageDown-86] - _ = x[actPrevHistory-87] - _ = x[actPrevSelected-88] - _ = x[actPut-89] - _ = x[actNextHistory-90] - _ = x[actNextSelected-91] - _ = x[actExecute-92] - _ = x[actExecuteSilent-93] - _ = x[actExecuteMulti-94] - _ = x[actSigStop-95] - _ = x[actFirst-96] - _ = x[actLast-97] - _ = x[actReload-98] - _ = x[actReloadSync-99] - _ = x[actDisableSearch-100] - _ = x[actEnableSearch-101] - _ = x[actSelect-102] - _ = x[actDeselect-103] - _ = x[actUnbind-104] - _ = x[actRebind-105] - _ = x[actBecome-106] - _ = x[actResponse-107] - _ = x[actShowHeader-108] - _ = x[actHideHeader-109] + _ = x[actFatal-29] + _ = x[actForwardChar-30] + _ = x[actForwardWord-31] + _ = x[actKillLine-32] + _ = x[actKillWord-33] + _ = x[actUnixLineDiscard-34] + _ = x[actUnixWordRubout-35] + _ = x[actYank-36] + _ = x[actBackwardKillWord-37] + _ = x[actSelectAll-38] + _ = x[actDeselectAll-39] + _ = x[actToggle-40] + _ = x[actToggleSearch-41] + _ = x[actToggleAll-42] + _ = x[actToggleDown-43] + _ = x[actToggleUp-44] + _ = x[actToggleIn-45] + _ = x[actToggleOut-46] + _ = x[actToggleTrack-47] + _ = x[actToggleTrackCurrent-48] + _ = x[actToggleHeader-49] + _ = x[actTrackCurrent-50] + _ = x[actUntrackCurrent-51] + _ = x[actDown-52] + _ = x[actUp-53] + _ = x[actPageUp-54] + _ = x[actPageDown-55] + _ = x[actPosition-56] + _ = x[actHalfPageUp-57] + _ = x[actHalfPageDown-58] + _ = x[actOffsetUp-59] + _ = x[actOffsetDown-60] + _ = x[actJump-61] + _ = x[actJumpAccept-62] + _ = x[actPrintQuery-63] + _ = x[actRefreshPreview-64] + _ = x[actReplaceQuery-65] + _ = x[actToggleSort-66] + _ = x[actShowPreview-67] + _ = x[actHidePreview-68] + _ = x[actTogglePreview-69] + _ = x[actTogglePreviewWrap-70] + _ = x[actTransform-71] + _ = x[actTransformBorderLabel-72] + _ = x[actTransformHeader-73] + _ = x[actTransformPreviewLabel-74] + _ = x[actTransformPrompt-75] + _ = x[actTransformQuery-76] + _ = x[actPreview-77] + _ = x[actChangePreview-78] + _ = x[actChangePreviewWindow-79] + _ = x[actPreviewTop-80] + _ = x[actPreviewBottom-81] + _ = x[actPreviewUp-82] + _ = x[actPreviewDown-83] + _ = x[actPreviewPageUp-84] + _ = x[actPreviewPageDown-85] + _ = x[actPreviewHalfPageUp-86] + _ = x[actPreviewHalfPageDown-87] + _ = x[actPrevHistory-88] + _ = x[actPrevSelected-89] + _ = x[actPut-90] + _ = x[actNextHistory-91] + _ = x[actNextSelected-92] + _ = x[actExecute-93] + _ = x[actExecuteSilent-94] + _ = x[actExecuteMulti-95] + _ = x[actSigStop-96] + _ = x[actFirst-97] + _ = x[actLast-98] + _ = x[actReload-99] + _ = x[actReloadSync-100] + _ = x[actDisableSearch-101] + _ = x[actEnableSearch-102] + _ = x[actSelect-103] + _ = x[actDeselect-104] + _ = x[actUnbind-105] + _ = x[actRebind-106] + _ = x[actBecome-107] + _ = x[actResponse-108] + _ = 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 { if i < 0 || i >= actionType(len(_actionType_index)-1) { diff --git a/src/algo/algo.go b/src/algo/algo.go index 3e68974..c85ec82 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -152,7 +152,7 @@ var ( // Extra bonus for word boundary after slash, colon, semi-colon, and comma bonusBoundaryDelimiter int16 = bonusBoundary + 1 - initialCharClass charClass = charWhite + initialCharClass = charWhite // A minor optimization that can give 15%+ performance boost asciiCharClasses [unicode.MaxASCII + 1]charClass diff --git a/src/algo/normalize.go b/src/algo/normalize.go index 9324790..25e9298 100644 --- a/src/algo/normalize.go +++ b/src/algo/normalize.go @@ -3,7 +3,7 @@ package algo -var normalized map[rune]rune = map[rune]rune{ +var normalized = map[rune]rune{ 0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER 0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER 0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER diff --git a/src/ansi.go b/src/ansi.go index 53fae95..dbe4fd6 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -292,7 +292,7 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo func parseAnsiCode(s string, delimiter byte) (int, byte, string) { var remaining string - i := -1 + var i int if delimiter == 0 { // Faster than strings.IndexAny(";:") i = strings.IndexByte(s, ';') @@ -350,7 +350,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState { state256 := 0 ptr := &state.fg - var delimiter byte = 0 + var delimiter byte count := 0 for len(ansiCode) != 0 { var num int diff --git a/src/cache.go b/src/cache.go index df1a6ab..39d4250 100644 --- a/src/cache.go +++ b/src/cache.go @@ -12,8 +12,14 @@ type ChunkCache struct { } // NewChunkCache returns a new ChunkCache -func NewChunkCache() ChunkCache { - return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)} +func NewChunkCache() *ChunkCache { + 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 diff --git a/src/constants.go b/src/constants.go index faf6a0e..dd2e870 100644 --- a/src/constants.go +++ b/src/constants.go @@ -67,9 +67,9 @@ const ( ) const ( - exitCancel = -1 - exitOk = 0 - exitNoMatch = 1 - exitError = 2 - exitInterrupt = 130 + ExitCancel = -1 + ExitOk = 0 + ExitNoMatch = 1 + ExitError = 2 + ExitInterrupt = 130 ) diff --git a/src/core.go b/src/core.go index 14aa781..dbae6a6 100644 --- a/src/core.go +++ b/src/core.go @@ -2,7 +2,6 @@ package fzf import ( - "fmt" "sync" "time" "unsafe" @@ -27,22 +26,29 @@ func sbytes(data string) []byte { return unsafe.Slice(unsafe.StringData(data), len(data)) } +type quitSignal struct { + code int + err error +} + // Run starts fzf -func Run(opts *Options, version string, revision string) { +func Run(opts *Options) (int, error) { + if err := postProcessOptions(opts); err != nil { + return ExitError, err + } + defer util.RunAtExitFuncs() + // Output channel given + if opts.Output != nil { + opts.Printer = func(str string) { + opts.Output <- str + } + } + sort := opts.Sort > 0 sortCriteria = opts.Criteria - if opts.Version { - if len(revision) > 0 { - fmt.Printf("%s (%s)\n", version, revision) - } else { - fmt.Println(version) - } - util.Exit(exitOk) - } - // Event channel eventBox := util.NewEventBox() @@ -131,7 +137,7 @@ func Run(opts *Options, version string, revision string) { reader = NewReader(func(data []byte) bool { return chunkList.Push(data) }, 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 @@ -147,14 +153,16 @@ func Run(opts *Options, version string, revision string) { forward = true } } + cache := NewChunkCache() + patternCache := make(map[string]*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.Filter == nil, opts.Nth, opts.Delimiter, runes) } inputRevision := 0 snapshotRevision := 0 - matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox, inputRevision) + matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision) // Filtering mode if opts.Filter != nil { @@ -182,7 +190,7 @@ func Run(opts *Options, version string, revision string) { } return 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 { eventBox.Unwatch(EvtReadNew) eventBox.WaitFor(EvtReadFin) @@ -197,9 +205,9 @@ func Run(opts *Options, version string, revision string) { } } if found { - util.Exit(exitOk) + return ExitOk, nil } - util.Exit(exitNoMatch) + return ExitNoMatch, nil } // Synchronous search @@ -210,9 +218,13 @@ func Run(opts *Options, version string, revision string) { // Go interactive go matcher.Loop() + defer matcher.Stop() // 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 padHeight := 0 heightUnknown := opts.Height.auto @@ -258,6 +270,9 @@ func Run(opts *Options, version string, revision string) { header = make([]string, 0, opts.HeaderLines) go reader.restart(command, environ) } + + exitCode := ExitOk + stop := false for { delay := true ticks++ @@ -278,7 +293,11 @@ func Run(opts *Options, version string, revision string) { if reading { reader.terminate() } - util.Exit(value.(int)) + quitSignal := value.(quitSignal) + exitCode = quitSignal.code + err = quitSignal.err + stop = true + return case EvtReadNew, EvtReadFin: if evt == EvtReadFin && nextCommand != nil { restart(*nextCommand, nextEnviron) @@ -378,10 +397,11 @@ func Run(opts *Options, version string, revision string) { for i := 0; i < count; i++ { opts.Printer(val.Get(i).item.AsString(opts.Ansi)) } - if count > 0 { - util.Exit(exitOk) + if count == 0 { + exitCode = ExitNoMatch } - util.Exit(exitNoMatch) + stop = true + return } determine(val.final) } @@ -392,6 +412,9 @@ func Run(opts *Options, version string, revision string) { } events.Clear() }) + if stop { + break + } if delay && reading { dur := util.DurWithin( time.Duration(ticks)*coordinatorDelayStep, @@ -399,4 +422,5 @@ func Run(opts *Options, version string, revision string) { time.Sleep(dur) } } + return exitCode, err } diff --git a/src/matcher.go b/src/matcher.go index b9288bb..26426e4 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -21,6 +21,7 @@ type MatchRequest struct { // Matcher is responsible for performing search type Matcher struct { + cache *ChunkCache patternBuilder func([]rune) *Pattern sort bool tac bool @@ -38,10 +39,11 @@ const ( ) // 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 { partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions) return &Matcher{ + cache: cache, patternBuilder: patternBuilder, sort: sort, tac: tac, @@ -60,8 +62,13 @@ func (m *Matcher) Loop() { for { var request MatchRequest + stop := false 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) { case MatchRequest: request = val @@ -71,12 +78,15 @@ func (m *Matcher) Loop() { } events.Clear() }) + if stop { + break + } if request.sort != m.sort || request.revision != m.revision { m.sort = request.sort m.revision = request.revision m.mergerCache = make(map[string]*Merger) - clearChunkCache() + m.cache.Clear() } // 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}) } + +func (m *Matcher) Stop() { + m.reqBox.Set(reqQuit, nil) +} diff --git a/src/options.go b/src/options.go index 5098318..9abecc3 100644 --- a/src/options.go +++ b/src/options.go @@ -1,6 +1,7 @@ package fzf import ( + "errors" "fmt" "os" "regexp" @@ -10,13 +11,12 @@ import ( "github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/tui" - "github.com/junegunn/fzf/src/util" "github.com/mattn/go-shellwords" "github.com/rivo/uniseg" ) -const usage = `usage: fzf [options] +const Usage = `usage: fzf [options] Search -x, --extended Extended-search mode @@ -247,9 +247,10 @@ func (o *previewOpts) Toggle() { o.hidden = !o.hidden } -func parseLabelPosition(opts *labelOpts, arg string) { +func parseLabelPosition(opts *labelOpts, arg string) error { opts.column = 0 opts.bottom = false + var err error for _, token := range splitRegexp.Split(strings.ToLower(arg), -1) { switch token { case "center": @@ -259,9 +260,10 @@ func parseLabelPosition(opts *labelOpts, arg string) { case "top": opts.bottom = false default: - opts.column = atoi(token) + opts.column, err = atoi(token) } } + return err } func (a previewOpts) aboveOrBelow() bool { @@ -291,6 +293,8 @@ type walkerOpts struct { // Options stores the values of command-line options type Options struct { + Input chan string + Output chan string Bash bool Zsh bool Fish bool @@ -365,6 +369,7 @@ type Options struct { WalkerRoot string WalkerSkip []string Version bool + Help bool CPUProfile string MEMProfile string BlockProfile string @@ -455,21 +460,10 @@ func defaultOptions() *Options { WalkerOpts: walkerOpts{file: true, hidden: true, follow: true}, WalkerRoot: ".", WalkerSkip: []string{".git", "node_modules"}, + Help: false, Version: false} } -func help(code int) { - os.Stdout.WriteString(usage) - util.Exit(code) -} - -var errorContext = "" - -func errorExit(msg string) { - os.Stderr.WriteString(errorContext + msg + "\n") - util.Exit(exitError) -} - func optString(arg string, prefixes ...string) (bool, string) { for _, prefix := range prefixes { if strings.HasPrefix(arg, prefix) { @@ -479,13 +473,13 @@ func optString(arg string, prefixes ...string) (bool, string) { return false, "" } -func nextString(args []string, i *int, message string) string { +func nextString(args []string, i *int, message string) (string, error) { if len(args) > *i+1 { *i++ } else { - errorExit(message) + return "", errors.New(message) } - return args[*i] + return args[*i], nil } func optionalNextString(args []string, i *int) (bool, string) { @@ -496,44 +490,52 @@ func optionalNextString(args []string, i *int) (bool, string) { return false, "" } -func atoi(str string) int { +func atoi(str string) (int, error) { num, err := strconv.Atoi(str) if err != nil { - errorExit("not a valid integer: " + str) + return 0, errors.New("not a valid integer: " + str) } - return num + return num, nil } -func atof(str string) float64 { +func atof(str string) (float64, error) { num, err := strconv.ParseFloat(str, 64) if err != nil { - errorExit("not a valid number: " + str) + return 0, errors.New("not a valid number: " + str) } - return num + return num, nil } -func nextInt(args []string, i *int, message string) int { +func nextInt(args []string, i *int, message string) (int, error) { if len(args) > *i+1 { *i++ } else { - errorExit(message) + return 0, errors.New(message) } - return atoi(args[*i]) + n, err := atoi(args[*i]) + if err != nil { + return 0, errors.New(message) + } + return n, nil } -func optionalNumeric(args []string, i *int, defaultValue int) int { +func optionalNumeric(args []string, i *int, defaultValue int) (int, error) { if len(args) > *i+1 { if strings.IndexAny(args[*i+1], "0123456789") == 0 { *i++ - return atoi(args[*i]) + n, err := atoi(args[*i]) + if err != nil { + return 0, err + } + return n, nil } } - return defaultValue + return defaultValue, nil } -func splitNth(str string) []Range { +func splitNth(str string) ([]Range, error) { if match, _ := regexp.MatchString("^[0-9,-.]+$", str); !match { - errorExit("invalid format: " + str) + return nil, errors.New("invalid format: " + str) } tokens := strings.Split(str, ",") @@ -541,11 +543,11 @@ func splitNth(str string) []Range { for idx, s := range tokens { r, ok := ParseRange(&s) if !ok { - errorExit("invalid format: " + str) + return nil, errors.New("invalid format: " + str) } ranges[idx] = r } - return ranges + return ranges, nil } func delimiterRegexp(str string) Delimiter { @@ -575,72 +577,68 @@ func isNumeric(char uint8) bool { return char >= '0' && char <= '9' } -func parseAlgo(str string) algo.Algo { +func parseAlgo(str string) (algo.Algo, error) { switch str { case "v1": - return algo.FuzzyMatchV1 + return algo.FuzzyMatchV1, nil case "v2": - return algo.FuzzyMatchV2 - default: - errorExit("invalid algorithm (expected: v1 or v2)") + return algo.FuzzyMatchV2, nil } - return algo.FuzzyMatchV2 + return nil, errors.New("invalid algorithm (expected: v1 or v2)") } -func processScheme(opts *Options) { +func processScheme(opts *Options) error { if !algo.Init(opts.Scheme) { - errorExit("invalid scoring scheme (expected: default|path|history)") + return errors.New("invalid scoring scheme (expected: default|path|history)") } if opts.Scheme == "history" { opts.Criteria = []criterion{byScore} } + return nil } -func parseBorder(str string, optional bool) tui.BorderShape { +func parseBorder(str string, optional bool) (tui.BorderShape, error) { switch str { case "rounded": - return tui.BorderRounded + return tui.BorderRounded, nil case "sharp": - return tui.BorderSharp + return tui.BorderSharp, nil case "bold": - return tui.BorderBold + return tui.BorderBold, nil case "block": - return tui.BorderBlock + return tui.BorderBlock, nil case "thinblock": - return tui.BorderThinBlock + return tui.BorderThinBlock, nil case "double": - return tui.BorderDouble + return tui.BorderDouble, nil case "horizontal": - return tui.BorderHorizontal + return tui.BorderHorizontal, nil case "vertical": - return tui.BorderVertical + return tui.BorderVertical, nil case "top": - return tui.BorderTop + return tui.BorderTop, nil case "bottom": - return tui.BorderBottom + return tui.BorderBottom, nil case "left": - return tui.BorderLeft + return tui.BorderLeft, nil case "right": - return tui.BorderRight + return tui.BorderRight, nil case "none": - return tui.BorderNone - default: - if optional && str == "" { - return tui.DefaultBorderShape - } - errorExit("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|none)") + return tui.BorderNone, nil } - return tui.BorderNone + if optional && str == "" { + return tui.DefaultBorderShape, nil + } + return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|none)") } -func parseKeyChords(str string, message string) map[tui.Event]string { - return parseKeyChordsImpl(str, message, errorExit) +func parseKeyChords(str string, message string) (map[tui.Event]string, error) { + return parseKeyChordsImpl(str, message) } -func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.Event]string { +func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error) { if len(str) == 0 { - exit(message) - return nil + return nil, errors.New(message) } str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string([]rune{escapedComma})) @@ -810,54 +808,64 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E } else if len(runes) == 1 { chords[tui.Key(runes[0])] = key } else { - exit("unsupported key: " + key) - return nil + return nil, errors.New("unsupported key: " + key) } } } - return chords + return chords, nil } -func parseTiebreak(str string) []criterion { +func parseTiebreak(str string) ([]criterion, error) { criteria := []criterion{byScore} hasIndex := false hasChunk := false hasLength := false hasBegin := false hasEnd := false - check := func(notExpected *bool, name string) { + check := func(notExpected *bool, name string) error { if *notExpected { - errorExit("duplicate sort criteria: " + name) + return errors.New("duplicate sort criteria: " + name) } if hasIndex { - errorExit("index should be the last criterion") + return errors.New("index should be the last criterion") } *notExpected = true + return nil } for _, str := range strings.Split(strings.ToLower(str), ",") { switch str { case "index": - check(&hasIndex, "index") + if err := check(&hasIndex, "index"); err != nil { + return nil, err + } case "chunk": - check(&hasChunk, "chunk") + if err := check(&hasChunk, "chunk"); err != nil { + return nil, err + } criteria = append(criteria, byChunk) case "length": - check(&hasLength, "length") + if err := check(&hasLength, "length"); err != nil { + return nil, err + } criteria = append(criteria, byLength) case "begin": - check(&hasBegin, "begin") + if err := check(&hasBegin, "begin"); err != nil { + return nil, err + } criteria = append(criteria, byBegin) case "end": - check(&hasEnd, "end") + if err := check(&hasEnd, "end"); err != nil { + return nil, err + } criteria = append(criteria, byEnd) default: - errorExit("invalid sort criterion: " + str) + return nil, errors.New("invalid sort criterion: " + str) } } if len(criteria) > 4 { - errorExit("at most 3 tiebreaks are allowed: " + str) + return nil, errors.New("at most 3 tiebreaks are allowed: " + str) } - return criteria + return criteria, nil } func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme { @@ -865,7 +873,8 @@ func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme { return &dupe } -func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { +func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, error) { + var err error theme := dupeTheme(defaultTheme) rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$") for _, str := range strings.Split(strings.ToLower(str), ",") { @@ -880,7 +889,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { theme = tui.NoColorTheme() default: fail := func() { - errorExit("invalid color specification: " + str) + // Let the code proceed to simplify the error handling + err = errors.New("invalid color specification: " + str) } // Color is disabled if theme == nil { @@ -1011,10 +1021,10 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { } } } - return theme + return theme, err } -func parseWalkerOpts(str string) walkerOpts { +func parseWalkerOpts(str string) (walkerOpts, error) { opts := walkerOpts{} for _, str := range strings.Split(strings.ToLower(str), ",") { switch str { @@ -1029,13 +1039,13 @@ func parseWalkerOpts(str string) walkerOpts { case "": // Ignored default: - errorExit("invalid walker option: " + str) + return opts, errors.New("invalid walker option: " + str) } } if !opts.file && !opts.dir { - errorExit("at least one of 'file' or 'dir' should be specified") + return opts, errors.New("at least one of 'file' or 'dir' should be specified") } - return opts + return opts, nil } var ( @@ -1079,7 +1089,7 @@ Loop: break } cs := string(action[0]) - ce := ")" + var ce string switch action[0] { case ':': masked += strings.Repeat(" ", len(action)) @@ -1120,13 +1130,13 @@ Loop: return masked } -func parseSingleActionList(str string, exit func(string)) []*action { +func parseSingleActionList(str string) ([]*action, error) { // We prepend a colon to satisfy executeRegexp and remove it later masked := maskActionContents(":" + str)[1:] - return parseActionList(masked, str, []*action{}, false, exit) + return parseActionList(masked, str, []*action{}, false) } -func parseActionList(masked string, original string, prevActions []*action, putAllowed bool, exit func(string)) []*action { +func parseActionList(masked string, original string, prevActions []*action, putAllowed bool) ([]*action, error) { maskedStrings := strings.Split(masked, "+") originalStrings := make([]string, len(maskedStrings)) idx := 0 @@ -1303,7 +1313,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA if putAllowed { appendAction(actChar) } else { - exit("unable to put non-printable character") + return nil, errors.New("unable to put non-printable character") } default: t := isExecuteAction(specLower) @@ -1313,7 +1323,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA } else if specLower == "change-multi" { appendAction(actChangeMulti) } else { - exit("unknown action: " + spec) + return nil, errors.New("unknown action: " + spec) } } else { offset := len(actionNameRegexp.FindString(spec)) @@ -1332,22 +1342,27 @@ func parseActionList(masked string, original string, prevActions []*action, putA } switch t { case actUnbind, actRebind: - parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit) + if _, err := parseKeyChordsImpl(actionArg, spec[0:offset]+" target required"); err != nil { + return nil, err + } case actChangePreviewWindow: opts := previewOpts{} for _, arg := range strings.Split(actionArg, "|") { // Make sure that each expression is valid - parsePreviewWindowImpl(&opts, arg, exit) + if err := parsePreviewWindowImpl(&opts, arg); err != nil { + return nil, err + } } } } } prevSpec = "" } - return actions + return actions, nil } -func parseKeymap(keymap map[tui.Event][]*action, str string, exit func(string)) { +func parseKeymap(keymap map[tui.Event][]*action, str string) error { + var err error masked := maskActionContents(str) idx := 0 for _, pairStr := range strings.Split(masked, ",") { @@ -1356,7 +1371,7 @@ func parseKeymap(keymap map[tui.Event][]*action, str string, exit func(string)) pair := strings.SplitN(pairStr, ":", 2) if len(pair) < 2 { - exit("bind action not specified: " + origPairStr) + return errors.New("bind action not specified: " + origPairStr) } var key tui.Event if len(pair[0]) == 1 && pair[0][0] == escapedColon { @@ -1366,12 +1381,19 @@ func parseKeymap(keymap map[tui.Event][]*action, str string, exit func(string)) } else if len(pair[0]) == 1 && pair[0][0] == escapedPlus { key = tui.Key('+') } else { - keys := parseKeyChordsImpl(pair[0], "key name required", exit) + keys, err := parseKeyChordsImpl(pair[0], "key name required") + if err != nil { + return err + } key = firstKey(keys) } putAllowed := key.Type == tui.Rune && unicode.IsGraphic(key.Char) - keymap[key] = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed, exit) + keymap[key], err = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed) + if err != nil { + return err + } } + return nil } func isExecuteAction(str string) actionType { @@ -1437,43 +1459,56 @@ func isExecuteAction(str string) actionType { return actIgnore } -func parseToggleSort(keymap map[tui.Event][]*action, str string) { - keys := parseKeyChords(str, "key name required") +func parseToggleSort(keymap map[tui.Event][]*action, str string) error { + keys, err := parseKeyChords(str, "key name required") + if err != nil { + return err + } if len(keys) != 1 { - errorExit("multiple keys specified") + return errors.New("multiple keys specified") } keymap[firstKey(keys)] = toActions(actToggleSort) + return nil } func strLines(str string) []string { return strings.Split(strings.TrimSuffix(str, "\n"), "\n") } -func parseSize(str string, maxPercent float64, label string) sizeSpec { +func parseSize(str string, maxPercent float64, label string) (sizeSpec, error) { + var spec = sizeSpec{} var val float64 + var err error percent := strings.HasSuffix(str, "%") if percent { - val = atof(str[:len(str)-1]) + if val, err = atof(str[:len(str)-1]); err != nil { + return spec, err + } + if val < 0 { - errorExit(label + " must be non-negative") + return spec, errors.New(label + " must be non-negative") } if val > maxPercent { - errorExit(fmt.Sprintf("%s too large (max: %d%%)", label, int(maxPercent))) + return spec, fmt.Errorf("%s too large (max: %d%%)", label, int(maxPercent)) } } else { if strings.Contains(str, ".") { - errorExit(label + " (without %) must be a non-negative integer") + return spec, errors.New(label + " (without %) must be a non-negative integer") } - val = float64(atoi(str)) + i, err := atoi(str) + if err != nil { + return spec, err + } + val = float64(i) if val < 0 { - errorExit(label + " must be non-negative") + return spec, errors.New(label + " must be non-negative") } } - return sizeSpec{val, percent} + return sizeSpec{val, percent}, nil } -func parseHeight(str string) heightSpec { +func parseHeight(str string) (heightSpec, error) { heightSpec := heightSpec{} if strings.HasPrefix(str, "~") { heightSpec.auto = true @@ -1481,66 +1516,66 @@ func parseHeight(str string) heightSpec { } if strings.HasPrefix(str, "-") { if heightSpec.auto { - errorExit("negative(-) height is not compatible with adaptive(~) height") + return heightSpec, errors.New("negative(-) height is not compatible with adaptive(~) height") } heightSpec.inverse = true str = str[1:] } - size := parseSize(str, 100, "height") + size, err := parseSize(str, 100, "height") + if err != nil { + return heightSpec, err + } heightSpec.size = size.size heightSpec.percent = size.percent - return heightSpec + return heightSpec, nil } -func parseLayout(str string) layoutType { +func parseLayout(str string) (layoutType, error) { switch str { case "default": - return layoutDefault + return layoutDefault, nil case "reverse": - return layoutReverse + return layoutReverse, nil case "reverse-list": - return layoutReverseList - default: - errorExit("invalid layout (expected: default / reverse / reverse-list)") + return layoutReverseList, nil } - return layoutDefault + return layoutDefault, errors.New("invalid layout (expected: default / reverse / reverse-list)") } -func parseInfoStyle(str string) (infoStyle, string) { +func parseInfoStyle(str string) (infoStyle, string, error) { switch str { case "default": - return infoDefault, "" + return infoDefault, "", nil case "right": - return infoRight, "" + return infoRight, "", nil case "inline": - return infoInline, defaultInfoPrefix + return infoInline, defaultInfoPrefix, nil case "inline-right": - return infoInlineRight, "" + return infoInlineRight, "", nil case "hidden": - return infoHidden, "" - default: - type infoSpec struct { - name string - style infoStyle - } - for _, spec := range []infoSpec{ - {"inline", infoInline}, - {"inline-right", infoInlineRight}} { - if strings.HasPrefix(str, spec.name+":") { - return spec.style, strings.ReplaceAll(str[len(spec.name)+1:], "\n", " ") - } - } - errorExit("invalid info style (expected: default|right|hidden|inline[-right][:PREFIX])") + return infoHidden, "", nil } - return infoDefault, "" + type infoSpec struct { + name string + style infoStyle + } + for _, spec := range []infoSpec{ + {"inline", infoInline}, + {"inline-right", infoInlineRight}} { + if strings.HasPrefix(str, spec.name+":") { + return spec.style, strings.ReplaceAll(str[len(spec.name)+1:], "\n", " "), nil + } + } + return infoDefault, "", errors.New("invalid info style (expected: default|right|hidden|inline[-right][:PREFIX])") } -func parsePreviewWindow(opts *previewOpts, input string) { - parsePreviewWindowImpl(opts, input, errorExit) +func parsePreviewWindow(opts *previewOpts, input string) error { + return parsePreviewWindowImpl(opts, input) } -func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string)) { +func parsePreviewWindowImpl(opts *previewOpts, input string) error { + var err error tokenRegex := regexp.MustCompile(`[:,]*(<([1-9][0-9]*)\(([^)<]+)\)|[^,:]+)`) sizeRegex := regexp.MustCompile("^[0-9]+%?$") offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`) @@ -1549,7 +1584,9 @@ func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string)) var alternative string for _, match := range tokens { if len(match[2]) > 0 { - opts.threshold = atoi(match[2]) + if opts.threshold, err = atoi(match[2]); err != nil { + return err + } alternative = match[3] continue } @@ -1610,14 +1647,17 @@ func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string)) opts.follow = false default: if headerRegex.MatchString(token) { - opts.headerLines = atoi(token[1:]) + if opts.headerLines, err = atoi(token[1:]); err != nil { + return err + } } else if sizeRegex.MatchString(token) { - opts.size = parseSize(token, 99, "window size") + if opts.size, err = parseSize(token, 99, "window size"); err != nil { + return err + } } else if offsetRegex.MatchString(token) { opts.scroll = token } else { - exit("invalid preview window option: " + token) - return + return errors.New("invalid preview window option: " + token) } } } @@ -1626,82 +1666,119 @@ func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string)) opts.alternative = &alternativeOpts opts.alternative.hidden = false opts.alternative.alternative = nil - parsePreviewWindowImpl(opts.alternative, alternative, exit) + err = parsePreviewWindowImpl(opts.alternative, alternative) } + return err } -func parseMargin(opt string, margin string) [4]sizeSpec { +func parseMargin(opt string, margin string) ([4]sizeSpec, error) { margins := strings.Split(margin, ",") - checked := func(str string) sizeSpec { + checked := func(str string) (sizeSpec, error) { return parseSize(str, 49, opt) } switch len(margins) { case 1: - m := checked(margins[0]) - return [4]sizeSpec{m, m, m, m} + m, e := checked(margins[0]) + return [4]sizeSpec{m, m, m, m}, e case 2: - tb := checked(margins[0]) - rl := checked(margins[1]) - return [4]sizeSpec{tb, rl, tb, rl} + tb, e := checked(margins[0]) + if e != nil { + return defaultMargin(), e + } + rl, e := checked(margins[1]) + if e != nil { + return defaultMargin(), e + } + return [4]sizeSpec{tb, rl, tb, rl}, nil case 3: - t := checked(margins[0]) - rl := checked(margins[1]) - b := checked(margins[2]) - return [4]sizeSpec{t, rl, b, rl} + t, e := checked(margins[0]) + if e != nil { + return defaultMargin(), e + } + rl, e := checked(margins[1]) + if e != nil { + return defaultMargin(), e + } + b, e := checked(margins[2]) + if e != nil { + return defaultMargin(), e + } + return [4]sizeSpec{t, rl, b, rl}, nil case 4: - return [4]sizeSpec{ - checked(margins[0]), checked(margins[1]), - checked(margins[2]), checked(margins[3])} - default: - errorExit("invalid " + opt + ": " + margin) + t, e := checked(margins[0]) + if e != nil { + return defaultMargin(), e + } + r, e := checked(margins[1]) + if e != nil { + return defaultMargin(), e + } + b, e := checked(margins[2]) + if e != nil { + return defaultMargin(), e + } + l, e := checked(margins[3]) + if e != nil { + return defaultMargin(), e + } + return [4]sizeSpec{t, r, b, l}, nil } - return defaultMargin() + return [4]sizeSpec{}, errors.New("invalid " + opt + ": " + margin) } -func parseOptions(opts *Options, allArgs []string) { +func parseOptions(opts *Options, allArgs []string) error { + var err error var historyMax int if opts.History == nil { historyMax = defaultHistoryMax } else { historyMax = opts.History.maxSize } - setHistory := func(path string) { + setHistory := func(path string) error { h, e := NewHistory(path, historyMax) if e != nil { - errorExit(e.Error()) + return e } opts.History = h + return nil } - setHistoryMax := func(max int) { + setHistoryMax := func(max int) error { historyMax = max if historyMax < 1 { - errorExit("history max must be a positive integer") + return errors.New("history max must be a positive integer") } if opts.History != nil { opts.History.maxSize = historyMax } + return nil } validateJumpLabels := false + clearExitingOpts := func() { + // Last-one-wins strategy + opts.Bash = false + opts.Zsh = false + opts.Fish = false + opts.Help = false + opts.Version = false + } for i := 0; i < len(allArgs); i++ { arg := allArgs[i] switch arg { case "--bash": + clearExitingOpts() opts.Bash = true - if opts.Zsh || opts.Fish { - errorExit("cannot specify --bash with --zsh or --fish") - } case "--zsh": + clearExitingOpts() opts.Zsh = true - if opts.Bash || opts.Fish { - errorExit("cannot specify --zsh with --bash or --fish") - } case "--fish": + clearExitingOpts() opts.Fish = true - if opts.Bash || opts.Zsh { - errorExit("cannot specify --fish with --bash or --zsh") - } case "-h", "--help": - help(exitOk) + clearExitingOpts() + opts.Help = true + case "--version": + clearExitingOpts() + opts.Version = true case "-x", "--extended": opts.Extended = true case "-e", "--exact": @@ -1715,20 +1792,43 @@ func parseOptions(opts *Options, allArgs []string) { case "+e", "--no-exact": opts.Fuzzy = true case "-q", "--query": - opts.Query = nextString(allArgs, &i, "query string required") + if opts.Query, err = nextString(allArgs, &i, "query string required"); err != nil { + return err + } case "-f", "--filter": - filter := nextString(allArgs, &i, "query string required") + filter, err := nextString(allArgs, &i, "query string required") + if err != nil { + return err + } opts.Filter = &filter case "--literal": opts.Normalize = false case "--no-literal": opts.Normalize = true case "--algo": - opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)")) + str, err := nextString(allArgs, &i, "algorithm required (v1|v2)") + if err != nil { + return err + } + if opts.FuzzyAlgo, err = parseAlgo(str); err != nil { + return err + } case "--scheme": - opts.Scheme = strings.ToLower(nextString(allArgs, &i, "scoring scheme required (default|path|history)")) + str, err := nextString(allArgs, &i, "scoring scheme required (default|path|history)") + if err != nil { + return err + } + opts.Scheme = strings.ToLower(str) case "--expect": - for k, v := range parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") { + str, err := nextString(allArgs, &i, "key names required") + if err != nil { + return err + } + chords, err := parseKeyChords(str, "key names required") + if err != nil { + return err + } + for k, v := range chords { opts.Expect[k] = v } case "--no-expect": @@ -1738,26 +1838,64 @@ func parseOptions(opts *Options, allArgs []string) { case "--disabled", "--phony": opts.Phony = true case "--tiebreak": - opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) + str, err := nextString(allArgs, &i, "sort criterion required") + if err != nil { + return err + } + if opts.Criteria, err = parseTiebreak(str); err != nil { + return err + } case "--bind": - parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"), errorExit) + str, err := nextString(allArgs, &i, "bind expression required") + if err != nil { + return err + } + if err := parseKeymap(opts.Keymap, str); err != nil { + return err + } case "--color": _, spec := optionalNextString(allArgs, &i) if len(spec) == 0 { opts.Theme = tui.EmptyTheme() } else { - opts.Theme = parseTheme(opts.Theme, spec) + if opts.Theme, err = parseTheme(opts.Theme, spec); err != nil { + return err + } } case "--toggle-sort": - parseToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required")) + str, err := nextString(allArgs, &i, "key name required") + if err != nil { + return err + } + if err := parseToggleSort(opts.Keymap, str); err != nil { + return err + } case "-d", "--delimiter": - opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) + str, err := nextString(allArgs, &i, "delimiter required") + if err != nil { + return err + } + opts.Delimiter = delimiterRegexp(str) case "-n", "--nth": - opts.Nth = splitNth(nextString(allArgs, &i, "nth expression required")) + str, err := nextString(allArgs, &i, "nth expression required") + if err != nil { + return err + } + if opts.Nth, err = splitNth(str); err != nil { + return err + } case "--with-nth": - opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required")) + str, err := nextString(allArgs, &i, "nth expression required") + if err != nil { + return err + } + if opts.WithNth, err = splitNth(str); err != nil { + return err + } case "-s", "--sort": - opts.Sort = optionalNumeric(allArgs, &i, 1) + if opts.Sort, err = optionalNumeric(allArgs, &i, 1); err != nil { + return err + } case "+s", "--no-sort": opts.Sort = 0 case "--track": @@ -1773,7 +1911,9 @@ func parseOptions(opts *Options, allArgs []string) { case "+i": opts.Case = CaseRespect case "-m", "--multi": - opts.Multi = optionalNumeric(allArgs, &i, maxMulti) + if opts.Multi, err = optionalNumeric(allArgs, &i, maxMulti); err != nil { + return err + } case "+m", "--no-multi": opts.Multi = 0 case "--ansi": @@ -1795,8 +1935,13 @@ func parseOptions(opts *Options, allArgs []string) { case "--no-bold": opts.Bold = false case "--layout": - opts.Layout = parseLayout( - nextString(allArgs, &i, "layout required (default / reverse / reverse-list)")) + str, err := nextString(allArgs, &i, "layout required (default / reverse / reverse-list)") + if err != nil { + return err + } + if opts.Layout, err = parseLayout(str); err != nil { + return err + } case "--reverse": opts.Layout = layoutReverse case "--no-reverse": @@ -1814,16 +1959,25 @@ func parseOptions(opts *Options, allArgs []string) { case "--no-hscroll": opts.Hscroll = false case "--hscroll-off": - opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required") + if opts.HscrollOff, err = nextInt(allArgs, &i, "hscroll offset required"); err != nil { + return err + } case "--scroll-off": - opts.ScrollOff = nextInt(allArgs, &i, "scroll offset required") + if opts.ScrollOff, err = nextInt(allArgs, &i, "scroll offset required"); err != nil { + return err + } case "--filepath-word": opts.FileWord = true case "--no-filepath-word": opts.FileWord = false case "--info": - opts.InfoStyle, opts.InfoPrefix = parseInfoStyle( - nextString(allArgs, &i, "info style required")) + str, err := nextString(allArgs, &i, "info style required") + if err != nil { + return err + } + if opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(str); err != nil { + return err + } case "--no-info": opts.InfoStyle = infoHidden case "--inline-info": @@ -1832,7 +1986,10 @@ func parseOptions(opts *Options, allArgs []string) { case "--no-inline-info": opts.InfoStyle = infoDefault case "--separator": - separator := nextString(allArgs, &i, "separator character required") + separator, err := nextString(allArgs, &i, "separator character required") + if err != nil { + return err + } opts.Separator = &separator case "--no-separator": nosep := "" @@ -1848,7 +2005,9 @@ func parseOptions(opts *Options, allArgs []string) { noBar := "" opts.Scrollbar = &noBar case "--jump-labels": - opts.JumpLabels = nextString(allArgs, &i, "label characters required") + if opts.JumpLabels, err = nextString(allArgs, &i, "label characters required"); err != nil { + return err + } validateJumpLabels = true case "-1", "--select-1": opts.Select1 = true @@ -1873,11 +2032,22 @@ func parseOptions(opts *Options, allArgs []string) { case "--no-print-query": opts.PrintQuery = false case "--prompt": - opts.Prompt = nextString(allArgs, &i, "prompt string required") + opts.Prompt, err = nextString(allArgs, &i, "prompt string required") + if err != nil { + return err + } case "--pointer": - opts.Pointer = firstLine(nextString(allArgs, &i, "pointer sign string required")) + str, err := nextString(allArgs, &i, "pointer sign string required") + if err != nil { + return err + } + opts.Pointer = firstLine(str) case "--marker": - opts.Marker = firstLine(nextString(allArgs, &i, "selected sign string required")) + str, err := nextString(allArgs, &i, "selected sign string required") + if err != nil { + return err + } + opts.Marker = firstLine(str) case "--sync": opts.Sync = true case "--no-sync", "--async": @@ -1885,35 +2055,69 @@ func parseOptions(opts *Options, allArgs []string) { case "--no-history": opts.History = nil case "--history": - setHistory(nextString(allArgs, &i, "history file path required")) + str, err := nextString(allArgs, &i, "history file path required") + if err != nil { + return err + } + if err := setHistory(str); err != nil { + return err + } case "--history-size": - setHistoryMax(nextInt(allArgs, &i, "history max size required")) + n, err := nextInt(allArgs, &i, "history max size required") + if err != nil { + return err + } + if err := setHistoryMax(n); err != nil { + return err + } case "--no-header": opts.Header = []string{} case "--no-header-lines": opts.HeaderLines = 0 case "--header": - opts.Header = strLines(nextString(allArgs, &i, "header string required")) + str, err := nextString(allArgs, &i, "header string required") + if err != nil { + return err + } + opts.Header = strLines(str) case "--header-lines": - opts.HeaderLines = atoi( - nextString(allArgs, &i, "number of header lines required")) + if opts.HeaderLines, err = nextInt(allArgs, &i, "number of header lines required"); err != nil { + return err + } case "--header-first": opts.HeaderFirst = true case "--no-header-first": opts.HeaderFirst = false case "--ellipsis": - opts.Ellipsis = nextString(allArgs, &i, "ellipsis string required") + if opts.Ellipsis, err = nextString(allArgs, &i, "ellipsis string required"); err != nil { + return err + } case "--preview": - opts.Preview.command = nextString(allArgs, &i, "preview command required") + if opts.Preview.command, err = nextString(allArgs, &i, "preview command required"); err != nil { + return err + } case "--no-preview": opts.Preview.command = "" case "--preview-window": - parsePreviewWindow(&opts.Preview, - nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-BORDER_OPT][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]")) + str, err := nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-BORDER_OPT][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]") + if err != nil { + return err + } + if err := parsePreviewWindow(&opts.Preview, str); err != nil { + return err + } case "--height": - opts.Height = parseHeight(nextString(allArgs, &i, "height required: [~]HEIGHT[%]")) + str, err := nextString(allArgs, &i, "height required: [~]HEIGHT[%]") + if err != nil { + return err + } + if opts.Height, err = parseHeight(str); err != nil { + return err + } case "--min-height": - opts.MinHeight = nextInt(allArgs, &i, "height required: HEIGHT") + if opts.MinHeight, err = nextInt(allArgs, &i, "height required: HEIGHT"); err != nil { + return err + } case "--no-height": opts.Height = heightSpec{} case "--no-margin": @@ -1924,21 +2128,38 @@ func parseOptions(opts *Options, allArgs []string) { opts.BorderShape = tui.BorderNone case "--border": hasArg, arg := optionalNextString(allArgs, &i) - opts.BorderShape = parseBorder(arg, !hasArg) + if opts.BorderShape, err = parseBorder(arg, !hasArg); err != nil { + return err + } case "--no-border-label": opts.BorderLabel.label = "" case "--border-label": - opts.BorderLabel.label = nextString(allArgs, &i, "label required") + opts.BorderLabel.label, err = nextString(allArgs, &i, "label required") + if err != nil { + return err + } case "--border-label-pos": - pos := nextString(allArgs, &i, "label position required (positive or negative integer or 'center')") - parseLabelPosition(&opts.BorderLabel, pos) + pos, err := nextString(allArgs, &i, "label position required (positive or negative integer or 'center')") + if err != nil { + return err + } + if err := parseLabelPosition(&opts.BorderLabel, pos); err != nil { + return err + } case "--no-preview-label": opts.PreviewLabel.label = "" case "--preview-label": - opts.PreviewLabel.label = nextString(allArgs, &i, "preview label required") + if opts.PreviewLabel.label, err = nextString(allArgs, &i, "preview label required"); err != nil { + return err + } case "--preview-label-pos": - pos := nextString(allArgs, &i, "preview label position required (positive or negative integer or 'center')") - parseLabelPosition(&opts.PreviewLabel, pos) + pos, err := nextString(allArgs, &i, "preview label position required (positive or negative integer or 'center')") + if err != nil { + return err + } + if err := parseLabelPosition(&opts.PreviewLabel, pos); err != nil { + return err + } case "--no-unicode": opts.Unicode = false case "--unicode": @@ -1948,17 +2169,29 @@ func parseOptions(opts *Options, allArgs []string) { case "--no-ambidouble": opts.Ambidouble = false case "--margin": - opts.Margin = parseMargin( - "margin", - nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) + str, err := nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)") + if err != nil { + return err + } + if opts.Margin, err = parseMargin("margin", str); err != nil { + return err + } case "--padding": - opts.Padding = parseMargin( - "padding", - nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) + str, err := nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)") + if err != nil { + return err + } + if opts.Padding, err = parseMargin("padding", str); err != nil { + return err + } case "--tabstop": - opts.Tabstop = nextInt(allArgs, &i, "tab stop required") + if opts.Tabstop, err = nextInt(allArgs, &i, "tab stop required"); err != nil { + return err + } case "--with-shell": - opts.WithShell = nextString(allArgs, &i, "shell command and flags required") + if opts.WithShell, err = nextString(allArgs, &i, "shell command and flags required"); err != nil { + return err + } case "--listen", "--listen-unsafe": given, str := optionalNextString(allArgs, &i) addr := defaultListenAddr @@ -1966,7 +2199,7 @@ func parseOptions(opts *Options, allArgs []string) { var err error addr, err = parseListenAddress(str) if err != nil { - errorExit(err.Error()) + return err } } opts.ListenAddr = &addr @@ -1979,26 +2212,46 @@ func parseOptions(opts *Options, allArgs []string) { case "--no-clear": opts.ClearOnExit = false case "--walker": - opts.WalkerOpts = parseWalkerOpts(nextString(allArgs, &i, "walker options required [file][,dir][,follow][,hidden]")) + str, err := nextString(allArgs, &i, "walker options required [file][,dir][,follow][,hidden]") + if err != nil { + return err + } + if opts.WalkerOpts, err = parseWalkerOpts(str); err != nil { + return err + } case "--walker-root": - opts.WalkerRoot = nextString(allArgs, &i, "directory required") + if opts.WalkerRoot, err = nextString(allArgs, &i, "directory required"); err != nil { + return err + } case "--walker-skip": - opts.WalkerSkip = filterNonEmpty(strings.Split(nextString(allArgs, &i, "directory names to ignore required"), ",")) - case "--version": - opts.Version = true + str, err := nextString(allArgs, &i, "directory names to ignore required") + if err != nil { + return err + } + opts.WalkerSkip = filterNonEmpty(strings.Split(str, ",")) case "--profile-cpu": - opts.CPUProfile = nextString(allArgs, &i, "file path required: cpu") + if opts.CPUProfile, err = nextString(allArgs, &i, "file path required: cpu"); err != nil { + return err + } case "--profile-mem": - opts.MEMProfile = nextString(allArgs, &i, "file path required: mem") + if opts.MEMProfile, err = nextString(allArgs, &i, "file path required: mem"); err != nil { + return err + } case "--profile-block": - opts.BlockProfile = nextString(allArgs, &i, "file path required: block") + if opts.BlockProfile, err = nextString(allArgs, &i, "file path required: block"); err != nil { + return err + } case "--profile-mutex": - opts.MutexProfile = nextString(allArgs, &i, "file path required: mutex") + if opts.MutexProfile, err = nextString(allArgs, &i, "file path required: mutex"); err != nil { + return err + } case "--": // Ignored default: if match, value := optString(arg, "--algo="); match { - opts.FuzzyAlgo = parseAlgo(value) + if opts.FuzzyAlgo, err = parseAlgo(value); err != nil { + return err + } } else if match, value := optString(arg, "--scheme="); match { opts.Scheme = strings.ToLower(value) } else if match, value := optString(arg, "-q", "--query="); match { @@ -2008,15 +2261,21 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, value := optString(arg, "-d", "--delimiter="); match { opts.Delimiter = delimiterRegexp(value) } else if match, value := optString(arg, "--border="); match { - opts.BorderShape = parseBorder(value, false) + if opts.BorderShape, err = parseBorder(value, false); err != nil { + return err + } } else if match, value := optString(arg, "--border-label="); match { opts.BorderLabel.label = value } else if match, value := optString(arg, "--border-label-pos="); match { - parseLabelPosition(&opts.BorderLabel, value) + if err := parseLabelPosition(&opts.BorderLabel, value); err != nil { + return err + } } else if match, value := optString(arg, "--preview-label="); match { opts.PreviewLabel.label = value } else if match, value := optString(arg, "--preview-label-pos="); match { - parseLabelPosition(&opts.PreviewLabel, value) + if err := parseLabelPosition(&opts.PreviewLabel, value); err != nil { + return err + } } else if match, value := optString(arg, "--prompt="); match { opts.Prompt = value } else if match, value := optString(arg, "--pointer="); match { @@ -2024,119 +2283,170 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, value := optString(arg, "--marker="); match { opts.Marker = firstLine(value) } else if match, value := optString(arg, "-n", "--nth="); match { - opts.Nth = splitNth(value) + if opts.Nth, err = splitNth(value); err != nil { + return err + } } else if match, value := optString(arg, "--with-nth="); match { - opts.WithNth = splitNth(value) + if opts.WithNth, err = splitNth(value); err != nil { + return err + } } else if match, _ := optString(arg, "-s", "--sort="); match { opts.Sort = 1 // Don't care } else if match, value := optString(arg, "-m", "--multi="); match { - opts.Multi = atoi(value) + if opts.Multi, err = atoi(value); err != nil { + return err + } } else if match, value := optString(arg, "--height="); match { - opts.Height = parseHeight(value) + if opts.Height, err = parseHeight(value); err != nil { + return err + } } else if match, value := optString(arg, "--min-height="); match { - opts.MinHeight = atoi(value) + if opts.MinHeight, err = atoi(value); err != nil { + return err + } } else if match, value := optString(arg, "--layout="); match { - opts.Layout = parseLayout(value) + if opts.Layout, err = parseLayout(value); err != nil { + return err + } } else if match, value := optString(arg, "--info="); match { - opts.InfoStyle, opts.InfoPrefix = parseInfoStyle(value) + if opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(value); err != nil { + return err + } } else if match, value := optString(arg, "--separator="); match { opts.Separator = &value } else if match, value := optString(arg, "--scrollbar="); match { opts.Scrollbar = &value } else if match, value := optString(arg, "--toggle-sort="); match { - parseToggleSort(opts.Keymap, value) + if err := parseToggleSort(opts.Keymap, value); err != nil { + return err + } } else if match, value := optString(arg, "--expect="); match { - for k, v := range parseKeyChords(value, "key names required") { + chords, err := parseKeyChords(value, "key names required") + if err != nil { + return err + } + for k, v := range chords { opts.Expect[k] = v } } else if match, value := optString(arg, "--tiebreak="); match { - opts.Criteria = parseTiebreak(value) + if opts.Criteria, err = parseTiebreak(value); err != nil { + return err + } } else if match, value := optString(arg, "--color="); match { - opts.Theme = parseTheme(opts.Theme, value) + if opts.Theme, err = parseTheme(opts.Theme, value); err != nil { + return err + } } else if match, value := optString(arg, "--bind="); match { - parseKeymap(opts.Keymap, value, errorExit) + if err := parseKeymap(opts.Keymap, value); err != nil { + return err + } } else if match, value := optString(arg, "--history="); match { - setHistory(value) + if err := setHistory(value); err != nil { + return err + } } else if match, value := optString(arg, "--history-size="); match { - setHistoryMax(atoi(value)) + n, err := atoi(value) + if err != nil { + return err + } + if err := setHistoryMax(n); err != nil { + return err + } } else if match, value := optString(arg, "--header="); match { opts.Header = strLines(value) } else if match, value := optString(arg, "--header-lines="); match { - opts.HeaderLines = atoi(value) + if opts.HeaderLines, err = atoi(value); err != nil { + return err + } } else if match, value := optString(arg, "--ellipsis="); match { opts.Ellipsis = value } else if match, value := optString(arg, "--preview="); match { opts.Preview.command = value } else if match, value := optString(arg, "--preview-window="); match { - parsePreviewWindow(&opts.Preview, value) + if err := parsePreviewWindow(&opts.Preview, value); err != nil { + return err + } } else if match, value := optString(arg, "--margin="); match { - opts.Margin = parseMargin("margin", value) + if opts.Margin, err = parseMargin("margin", value); err != nil { + return err + } } else if match, value := optString(arg, "--padding="); match { - opts.Padding = parseMargin("padding", value) + if opts.Padding, err = parseMargin("padding", value); err != nil { + return err + } } else if match, value := optString(arg, "--tabstop="); match { - opts.Tabstop = atoi(value) + if opts.Tabstop, err = atoi(value); err != nil { + return err + } } else if match, value := optString(arg, "--with-shell="); match { opts.WithShell = value } else if match, value := optString(arg, "--listen="); match { addr, err := parseListenAddress(value) if err != nil { - errorExit(err.Error()) + return err } opts.ListenAddr = &addr opts.Unsafe = false } else if match, value := optString(arg, "--listen-unsafe="); match { addr, err := parseListenAddress(value) if err != nil { - errorExit(err.Error()) + return err } opts.ListenAddr = &addr opts.Unsafe = true } else if match, value := optString(arg, "--walker="); match { - opts.WalkerOpts = parseWalkerOpts(value) + if opts.WalkerOpts, err = parseWalkerOpts(value); err != nil { + return err + } } else if match, value := optString(arg, "--walker-root="); match { opts.WalkerRoot = value } else if match, value := optString(arg, "--walker-skip="); match { opts.WalkerSkip = filterNonEmpty(strings.Split(value, ",")) } else if match, value := optString(arg, "--hscroll-off="); match { - opts.HscrollOff = atoi(value) + if opts.HscrollOff, err = atoi(value); err != nil { + return err + } } else if match, value := optString(arg, "--scroll-off="); match { - opts.ScrollOff = atoi(value) + if opts.ScrollOff, err = atoi(value); err != nil { + return err + } } else if match, value := optString(arg, "--jump-labels="); match { opts.JumpLabels = value validateJumpLabels = true } else { - errorExit("unknown option: " + arg) + return errors.New("unknown option: " + arg) } } } if opts.HeaderLines < 0 { - errorExit("header lines must be a non-negative integer") + return errors.New("header lines must be a non-negative integer") } if opts.HscrollOff < 0 { - errorExit("hscroll offset must be a non-negative integer") + return errors.New("hscroll offset must be a non-negative integer") } if opts.ScrollOff < 0 { - errorExit("scroll offset must be a non-negative integer") + return errors.New("scroll offset must be a non-negative integer") } if opts.Tabstop < 1 { - errorExit("tab stop must be a positive integer") + return errors.New("tab stop must be a positive integer") } if len(opts.JumpLabels) == 0 { - errorExit("empty jump labels") + return errors.New("empty jump labels") } if validateJumpLabels { for _, r := range opts.JumpLabels { if r < 32 || r > 126 { - errorExit("non-ascii jump labels are not allowed") + return errors.New("non-ascii jump labels are not allowed") } } } + return err } func validateSign(sign string, signOptName string) error { @@ -2149,31 +2459,33 @@ func validateSign(sign string, signOptName string) error { return nil } -func postProcessOptions(opts *Options) { +// This function can have side-effects and alter some global states. +// So we run it on fzf.Run and not on ParseOptions. +func postProcessOptions(opts *Options) error { if opts.Ambidouble { uniseg.EastAsianAmbiguousWidth = 2 } if err := validateSign(opts.Pointer, "pointer"); err != nil { - errorExit(err.Error()) + return err } if err := validateSign(opts.Marker, "marker"); err != nil { - errorExit(err.Error()) + return err } - if !opts.Version && !tui.IsLightRendererSupported() && opts.Height.size > 0 { - errorExit("--height option is currently not supported on this platform") + if !tui.IsLightRendererSupported() && opts.Height.size > 0 { + return errors.New("--height option is currently not supported on this platform") } if opts.Scrollbar != nil { runes := []rune(*opts.Scrollbar) if len(runes) > 2 { - errorExit("--scrollbar should be given one or two characters") + return errors.New("--scrollbar should be given one or two characters") } for _, r := range runes { if uniseg.StringWidth(string(r)) != 1 { - errorExit("scrollbar display width should be 1") + return errors.New("scrollbar display width should be 1") } } } @@ -2227,12 +2539,12 @@ func postProcessOptions(opts *Options) { if opts.Height.auto { for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} { if s.percent { - errorExit("adaptive height is not compatible with top/bottom percent margin") + return errors.New("adaptive height is not compatible with top/bottom percent margin") } } for _, s := range []sizeSpec{opts.Padding[0], opts.Padding[2]} { if s.percent { - errorExit("adaptive height is not compatible with top/bottom percent padding") + return errors.New("adaptive height is not compatible with top/bottom percent padding") } } } @@ -2243,7 +2555,7 @@ func postProcessOptions(opts *Options) { for _, r := range opts.Nth { if r.begin == rangeEllipsis && r.end == rangeEllipsis { opts.Nth = make([]Range, 0) - return + break } } } @@ -2265,65 +2577,52 @@ func postProcessOptions(opts *Options) { theme.Spinner = boldify(theme.Spinner) } - processScheme(opts) -} - -func expectsArbitraryString(opt string) bool { - switch opt { - case "-q", "--query", "-f", "--filter", "--header", "--prompt": - return true - } - return false + return processScheme(opts) } // ParseOptions parses command-line options -func ParseOptions() *Options { +func ParseOptions(useDefaults bool, args []string) (*Options, error) { opts := defaultOptions() - for idx, arg := range os.Args[1:] { - if arg == "--version" && (idx == 0 || idx > 0 && !expectsArbitraryString(os.Args[idx])) { - opts.Version = true - return opts - } - } + if useDefaults { + // 1. Options from $FZF_DEFAULT_OPTS_FILE + if path := os.Getenv("FZF_DEFAULT_OPTS_FILE"); path != "" { + bytes, err := os.ReadFile(path) + if err != nil { + return nil, errors.New("$FZF_DEFAULT_OPTS_FILE: " + err.Error()) + } - // 1. Options from $FZF_DEFAULT_OPTS_FILE - if path := os.Getenv("FZF_DEFAULT_OPTS_FILE"); path != "" { - bytes, err := os.ReadFile(path) - if err != nil { - errorContext = "$FZF_DEFAULT_OPTS_FILE: " - errorExit(err.Error()) + words, parseErr := shellwords.Parse(string(bytes)) + if parseErr != nil { + return nil, errors.New(path + ": " + parseErr.Error()) + } + if len(words) > 0 { + if err := parseOptions(opts, words); err != nil { + return nil, errors.New(path + ": " + err.Error()) + } + } } - words, parseErr := shellwords.Parse(string(bytes)) + // 2. Options from $FZF_DEFAULT_OPTS string + words, parseErr := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS")) if parseErr != nil { - errorContext = path + ": " - errorExit(parseErr.Error()) + return nil, errors.New("$FZF_DEFAULT_OPTS: " + parseErr.Error()) } if len(words) > 0 { - parseOptions(opts, words) + if err := parseOptions(opts, words); err != nil { + return nil, errors.New("$FZF_DEFAULT_OPTS: " + err.Error()) + } } } - // 2. Options from $FZF_DEFAULT_OPTS string - words, parseErr := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS")) - errorContext = "$FZF_DEFAULT_OPTS: " - if parseErr != nil { - errorExit(parseErr.Error()) - } - if len(words) > 0 { - parseOptions(opts, words) - } - // 3. Options from command-line arguments - errorContext = "" - parseOptions(opts, os.Args[1:]) - - if err := opts.initProfiling(); err != nil { - errorExit("failed to start pprof profiles: " + err.Error()) + if err := parseOptions(opts, args); err != nil { + return nil, err } - postProcessOptions(opts) + if err := opts.initProfiling(); err != nil { + return nil, errors.New("failed to start pprof profiles: " + err.Error()) + } - return opts + return opts, nil } diff --git a/src/options_no_pprof.go b/src/options_no_pprof.go index 1a19bc6..1093fc1 100644 --- a/src/options_no_pprof.go +++ b/src/options_no_pprof.go @@ -3,9 +3,11 @@ package fzf +import "errors" + func (o *Options) initProfiling() error { 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 } diff --git a/src/options_test.go b/src/options_test.go index 8e3b20f..270af5c 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -80,7 +80,7 @@ func TestDelimiterRegexRegexCaret(t *testing.T) { func TestSplitNth(t *testing.T) { { - ranges := splitNth("..") + ranges, _ := splitNth("..") if len(ranges) != 1 || ranges[0].begin != 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 || ranges[0].begin != rangeEllipsis || ranges[0].end != 3 || ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis || @@ -137,7 +137,7 @@ func TestIrrelevantNth(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) { if pairs[e] != s { t.Errorf("%s != %s", pairs[e], s) @@ -163,7 +163,7 @@ func TestParseKeys(t *testing.T) { checkEvent(tui.AltKey(' '), "alt-SPACE") // 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 { t.Error(9) } @@ -177,7 +177,7 @@ func TestParseKeys(t *testing.T) { check(tui.Left, "left") 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 { t.Error(11) } @@ -206,40 +206,40 @@ func TestParseKeysWithComma(t *testing.T) { } } - pairs := parseKeyChords(",", "") + pairs, _ := parseKeyChords(",", "") checkN(len(pairs), 1) check(pairs, tui.Key(','), ",") - pairs = parseKeyChords(",,a,b", "") + pairs, _ = parseKeyChords(",,a,b", "") checkN(len(pairs), 3) check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('b'), "b") check(pairs, tui.Key(','), ",") - pairs = parseKeyChords("a,b,,", "") + pairs, _ = parseKeyChords("a,b,,", "") checkN(len(pairs), 3) check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('b'), "b") check(pairs, tui.Key(','), ",") - pairs = parseKeyChords("a,,,b", "") + pairs, _ = parseKeyChords("a,,,b", "") checkN(len(pairs), 3) check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('b'), "b") check(pairs, tui.Key(','), ",") - pairs = parseKeyChords("a,,,b,c", "") + pairs, _ = parseKeyChords("a,,,b,c", "") checkN(len(pairs), 4) check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('c'), "c") check(pairs, tui.Key(','), ",") - pairs = parseKeyChords(",,,", "") + pairs, _ = parseKeyChords(",,,", "") checkN(len(pairs), 1) check(pairs, tui.Key(','), ",") - pairs = parseKeyChords(",ALT-,,", "") + pairs, _ = parseKeyChords(",ALT-,,", "") checkN(len(pairs), 1) check(pairs, tui.AltKey(','), "ALT-,") } @@ -262,17 +262,13 @@ func TestBind(t *testing.T) { } } check(tui.CtrlA.AsEvent(), "", actBeginningOfLine) - errorString := "" - errorFn := func(e string) { - errorString = e - } parseKeymap(keymap, "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 {};,"+ "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+ "x:Execute(foo+bar),X:execute/bar+baz/"+ ",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.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown) check(tui.Key('c'), "", actPageUp) @@ -290,20 +286,17 @@ func TestBind(t *testing.T) { check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute) 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) } - parseKeymap(keymap, "f1:abort", errorFn) + parseKeymap(keymap, "f1:abort") check(tui.F1.AsEvent(), "", actAbort) - if len(errorString) > 0 { - t.Errorf("error parsing keymap: %s", errorString) - } } func TestColorSpec(t *testing.T) { theme := tui.Dark256 - dark := parseTheme(theme, "dark") + dark, _ := parseTheme(theme, "dark") if *dark != *theme { t.Errorf("colors should be equivalent") } @@ -311,7 +304,7 @@ func TestColorSpec(t *testing.T) { t.Errorf("point should not be equivalent") } - light := parseTheme(theme, "dark,light") + light, _ := parseTheme(theme, "dark,light") if *light == *theme { t.Errorf("should not be equivalent") } @@ -322,7 +315,7 @@ func TestColorSpec(t *testing.T) { 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 { 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) } - 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 { t.Errorf("color not customized") } @@ -475,7 +468,7 @@ func TestValidateSign(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 { t.Errorf("Invalid number of actions parsed:%d", len(actions)) } @@ -491,11 +484,8 @@ func TestParseSingleActionList(t *testing.T) { } func TestParseSingleActionListError(t *testing.T) { - err := "" - parseSingleActionList("change-query(foobar)baz", func(e string) { - err = e - }) - if len(err) == 0 { + _, err := parseSingleActionList("change-query(foobar)baz") + if err == nil { t.Errorf("Failed to detect error") } } diff --git a/src/pattern.go b/src/pattern.go index bf92ca1..cbe73dc 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -60,32 +60,17 @@ type Pattern struct { delimiter Delimiter nth []Range procFun map[termType]algo.Algo + cache *ChunkCache } -var ( - _patternCache map[string]*Pattern - _splitRegex *regexp.Regexp - _cache ChunkCache -) +var _splitRegex *regexp.Regexp func init() { _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 -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 { var asString string @@ -98,7 +83,9 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, 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 { return cached } @@ -153,6 +140,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, cacheable: cacheable, nth: nth, delimiter: delimiter, + cache: cache, procFun: make(map[termType]algo.Algo)} 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[termSuffix] = algo.SuffixMatch - _patternCache[asString] = ptr + patternCache[asString] = ptr return ptr } @@ -282,18 +270,18 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result { // ChunkCache: Exact match cacheKey := p.CacheKey() if p.cacheable { - if cached := _cache.Lookup(chunk, cacheKey); cached != nil { + if cached := p.cache.Lookup(chunk, cacheKey); cached != nil { return cached } } // Prefix/suffix cache - space := _cache.Search(chunk, cacheKey) + space := p.cache.Search(chunk, cacheKey) matches := p.matchChunk(chunk, space, slab) if p.cacheable { - _cache.Add(chunk, cacheKey, matches) + p.cache.Add(chunk, cacheKey, matches) } return matches } diff --git a/src/pattern_test.go b/src/pattern_test.go index 5eb5f6d..9f105f6 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -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) { - defer clearPatternCache() - clearPatternCache() - pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, + pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("'abc")) chars := util.ToChars([]byte("aabbcc abc")) res, pos := algo.ExactMatchNaive( @@ -81,9 +86,7 @@ func TestExact(t *testing.T) { } func TestEqual(t *testing.T) { - defer clearPatternCache() - clearPatternCache() - pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$")) + pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$")) match := func(str string, sidxExpected int, eidxExpected int) { chars := util.ToChars([]byte(str)) @@ -104,19 +107,12 @@ func TestEqual(t *testing.T) { } func TestCaseSensitivity(t *testing.T) { - defer clearPatternCache() - 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")) - clearPatternCache() - 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")) + pat1 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc")) + pat2 := 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")) + pat4 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc")) + pat5 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc")) + pat6 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc")) if string(pat1.text) != "abc" || pat1.caseSensitive != false || string(pat2.text) != "Abc" || pat2.caseSensitive != true || @@ -129,7 +125,7 @@ func TestCaseSensitivity(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{}) trans := Transform(tokens, []Range{{1, 1}}) @@ -163,15 +159,13 @@ func TestOrigTextAndTransformed(t *testing.T) { func TestCacheKey(t *testing.T) { 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 { t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) } if pat.cacheable != cacheable { t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr) } - clearPatternCache() } test(false, "foo !bar", "foo !bar", true) test(false, "foo | bar !baz", "foo | bar !baz", true) @@ -187,15 +181,13 @@ func TestCacheKey(t *testing.T) { func TestCacheable(t *testing.T) { 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 { t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) } if cacheable != pat.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", false) diff --git a/src/protector/protector.go b/src/protector/protector.go index fe84b38..9758e1e 100644 --- a/src/protector/protector.go +++ b/src/protector/protector.go @@ -3,6 +3,4 @@ package protector // Protect calls OS specific protections like pledge on OpenBSD -func Protect() { - return -} +func Protect() {} diff --git a/src/reader.go b/src/reader.go index 8fa864e..6092087 100644 --- a/src/reader.go +++ b/src/reader.go @@ -93,11 +93,26 @@ func (r *Reader) restart(command string, environ []string) { 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 -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() var success bool - if util.IsTty() { + if inputChan != nil { + success = r.readChannel(inputChan) + } else if util.IsTty() { cmd := os.Getenv("FZF_DEFAULT_COMMAND") if len(cmd) == 0 { success = r.readFiles(root, opts, ignores) diff --git a/src/server.go b/src/server.go index aa80eb4..0f325d8 100644 --- a/src/server.go +++ b/src/server.go @@ -73,28 +73,28 @@ func parseListenAddress(address string) (listenAddress, error) { 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 port := address.port apiKey := os.Getenv("FZF_API_KEY") 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) listener, err := net.Listen("tcp", addrStr) 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 { addr := listener.Addr().String() parts := strings.Split(addr, ":") 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 port, err = strconv.Atoi(parts[len(parts)-1]) 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() if err != nil { if errors.Is(err, net.ErrClosed) { - break - } else { - continue + return } + continue } conn.Write([]byte(server.handleHttpRequest(conn))) conn.Close() } - listener.Close() }() - return port, nil + return listener, port, nil } // 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] - errorMessage := "" - actions := parseSingleActionList(strings.Trim(string(body), "\r\n"), func(message string) { - errorMessage = message - }) - if len(errorMessage) > 0 { - return bad(errorMessage) + actions, err := parseSingleActionList(strings.Trim(string(body), "\r\n")) + if err != nil { + return bad(err.Error()) } if len(actions) == 0 { return bad("no action specified") diff --git a/src/terminal.go b/src/terminal.go index 951b8c3..1891503 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -2,10 +2,12 @@ package fzf import ( "bufio" + "context" "encoding/json" "fmt" "io" "math" + "net" "os" "os/signal" "regexp" @@ -49,8 +51,8 @@ var whiteSuffix *regexp.Regexp var offsetComponentRegex *regexp.Regexp var offsetTrimCharsRegex *regexp.Regexp var activeTempFiles []string +var activeTempFilesMutex sync.Mutex var passThroughRegex *regexp.Regexp -var actionTypeRegex *regexp.Regexp const clearCode string = "\x1b[2J" @@ -63,6 +65,7 @@ func init() { offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`) offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`) activeTempFiles = []string{} + activeTempFilesMutex = sync.Mutex{} // 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 @@ -241,6 +244,7 @@ type Terminal struct { unicode bool listenAddr *listenAddress listenPort *int + listener net.Listener listenUnsafe bool borderShape tui.BorderShape cleanExit bool @@ -259,7 +263,7 @@ type Terminal struct { hasResizeActions bool triggerLoad bool reading bool - running bool + running *util.AtomicBool failed *string jumping jumpMode jumpLabels string @@ -278,12 +282,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 @@ -340,6 +344,7 @@ const ( reqPreviewRefresh reqPreviewDelayed reqQuit + reqFatal ) type action struct { @@ -380,6 +385,7 @@ const ( actDeleteChar actDeleteCharEof actEndOfLine + actFatal actForwardChar actForwardWord actKillLine @@ -511,6 +517,7 @@ type previewRequest struct { pwindowSize tui.TermSize scrollOffset int list []*Item + env []string } type previewResult struct { @@ -537,6 +544,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) @@ -642,7 +650,7 @@ func evaluateHeight(opts *Options, termHeight int) int { } // 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) var delay time.Duration if opts.Tac { @@ -660,11 +668,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor } 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 { @@ -680,7 +689,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor 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]|(.$)" @@ -693,6 +705,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor for key, action := range opts.Keymap { keymapCopy[key] = action } + t := Terminal{ initDelay: delay, infoStyle: opts.InfoStyle, @@ -754,7 +767,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor hasLoadActions: false, triggerLoad: false, reading: true, - running: true, + running: util.NewAtomicBool(true), failed: nil, jumping: jumpDisabled, jumpLabels: opts.JumpLabels, @@ -775,12 +788,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor 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()} @@ -832,14 +845,15 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor _, 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 { @@ -2103,12 +2117,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) @@ -2232,9 +2245,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) } } @@ -2485,8 +2497,6 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) { case 'q': flags.forceUpdate = true // 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 { f, err := os.CreateTemp("", "fzf-preview-*") if err != nil { - errorExit("Unable to create temporary file") + // Unable to create temporary file + // FIXME: Should we terminate the program? + return "" } defer f.Close() f.WriteString(strings.Join(data, printSep)) f.WriteString(printSep) + activeTempFilesMutex.Lock() activeTempFiles = append(activeTempFiles, f.Name()) + activeTempFilesMutex.Unlock() return f.Name() } func cleanTemporaryFiles() { + activeTempFilesMutex.Lock() for _, filename := range activeTempFiles { os.Remove(filename) } activeTempFiles = []string{} + activeTempFilesMutex.Unlock() } type replacePlaceholderParams struct { @@ -2836,18 +2852,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 { @@ -2871,7 +2887,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 @@ -2895,14 +2911,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) + } } } }() @@ -2911,8 +2936,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) + } } }() @@ -2921,14 +2950,23 @@ 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() @@ -2945,7 +2983,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() @@ -2960,15 +2998,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 @@ -2976,17 +3019,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 := t.executor.ExecCommand(command, true) - env := t.environ() if pwindowSize.Lines > 0 { lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines) columns := fmt.Sprintf("COLUMNS=%d", pwindowSize.Columns) @@ -3071,12 +3117,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 @@ -3123,19 +3170,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 @@ -3203,9 +3256,9 @@ func (t *Terminal) Loop() { case reqClose: exit(func() int { if t.output() { - return exitOk + return ExitOk } - return exitNoMatch + return ExitNoMatch }) return case reqPreviewDisplay: @@ -3233,11 +3286,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 } } @@ -3245,8 +3301,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 @@ -3256,8 +3315,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 @@ -3353,7 +3420,7 @@ func (t *Terminal) Loop() { t.pressed = ret t.reqBox.Set(reqClose, nil) t.mutex.Unlock() - return + return nil } } @@ -3362,8 +3429,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 { @@ -3433,7 +3499,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 @@ -3547,8 +3613,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 @@ -3580,6 +3647,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: @@ -4005,10 +4074,10 @@ func (t *Terminal) Loop() { } if me.Down { - mx_cons := util.Constrain(mx-t.promptLen, 0, len(t.input)) - if my == t.promptLine() && mx_cons >= 0 { + mxCons := util.Constrain(mx-t.promptLen, 0, len(t.input)) + if my == t.promptLine() && mxCons >= 0 { // Prompt - t.cx = mx_cons + t.xoffset + t.cx = mxCons + t.xoffset } else if my >= min { t.vset(t.offset + my - min) req(reqList) @@ -4066,15 +4135,17 @@ func (t *Terminal) Loop() { 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: @@ -4221,6 +4292,7 @@ func (t *Terminal) Loop() { t.reqBox.Set(event, nil) } } + return nil } func (t *Terminal) constrain() { diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go index 985cef9..3119f79 100644 --- a/src/tokenizer_test.go +++ b/src/tokenizer_test.go @@ -71,14 +71,14 @@ func TestTransform(t *testing.T) { { tokens := Tokenize(input, Delimiter{}) { - ranges := splitNth("1,2,3") + ranges, _ := splitNth("1,2,3") tx := Transform(tokens, ranges) if joinTokens(tx) != "abc: def: ghi: " { t.Errorf("%s", tx) } } { - ranges := splitNth("1..2,3,2..,1") + ranges, _ := splitNth("1..2,3,2..,1") tx := Transform(tokens, ranges) if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " || len(tx) != 4 || @@ -93,7 +93,7 @@ func TestTransform(t *testing.T) { { tokens := Tokenize(input, delimiterRegexp(":")) { - ranges := splitNth("1..2,3,2..,1") + ranges, _ := splitNth("1..2,3,2..,1") tx := Transform(tokens, ranges) if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" || len(tx) != 4 || @@ -108,5 +108,6 @@ func TestTransform(t *testing.T) { } func TestTransformIndexOutOfBounds(t *testing.T) { - Transform([]Token{}, splitNth("1")) + s, _ := splitNth("1") + Transform([]Token{}, s) } diff --git a/src/tui/dummy.go b/src/tui/dummy.go index 7760a72..1a76146 100644 --- a/src/tui/dummy.go +++ b/src/tui/dummy.go @@ -8,7 +8,7 @@ func HasFullscreenRenderer() bool { return false } -var DefaultBorderShape BorderShape = BorderRounded +var DefaultBorderShape = BorderRounded func (a Attr) Merge(b Attr) Attr { return a | b @@ -29,7 +29,7 @@ const ( 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) Pause(bool) {} func (r *FullscreenRenderer) Resume(bool, bool) {} diff --git a/src/tui/eventtype_string.go b/src/tui/eventtype_string.go index ce34d36..d752163 100644 --- a/src/tui/eventtype_string.go +++ b/src/tui/eventtype_string.go @@ -83,34 +83,35 @@ func _() { _ = x[Alt-72] _ = x[CtrlAlt-73] _ = x[Invalid-74] - _ = x[Mouse-75] - _ = x[DoubleClick-76] - _ = x[LeftClick-77] - _ = x[RightClick-78] - _ = x[SLeftClick-79] - _ = x[SRightClick-80] - _ = x[ScrollUp-81] - _ = x[ScrollDown-82] - _ = x[SScrollUp-83] - _ = x[SScrollDown-84] - _ = x[PreviewScrollUp-85] - _ = x[PreviewScrollDown-86] - _ = x[Resize-87] - _ = x[Change-88] - _ = x[BackwardEOF-89] - _ = x[Start-90] - _ = x[Load-91] - _ = x[Focus-92] - _ = x[One-93] - _ = x[Zero-94] - _ = x[Result-95] - _ = x[Jump-96] - _ = x[JumpCancel-97] + _ = x[Fatal-75] + _ = x[Mouse-76] + _ = x[DoubleClick-77] + _ = x[LeftClick-78] + _ = x[RightClick-79] + _ = x[SLeftClick-80] + _ = x[SRightClick-81] + _ = x[ScrollUp-82] + _ = x[ScrollDown-83] + _ = x[SScrollUp-84] + _ = x[SScrollDown-85] + _ = x[PreviewScrollUp-86] + _ = x[PreviewScrollDown-87] + _ = x[Resize-88] + _ = x[Change-89] + _ = x[BackwardEOF-90] + _ = x[Start-91] + _ = x[Load-92] + _ = x[Focus-93] + _ = x[One-94] + _ = x[Zero-95] + _ = x[Result-96] + _ = 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 { if i < 0 || i >= EventType(len(_EventType_index)-1) { diff --git a/src/tui/light.go b/src/tui/light.go index a045b78..3ef8b60 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -2,6 +2,7 @@ package tui import ( "bytes" + "errors" "fmt" "os" "regexp" @@ -10,6 +11,7 @@ import ( "time" "unicode/utf8" + "github.com/junegunn/fzf/src/util" "github.com/rivo/uniseg" "golang.org/x/term" @@ -27,8 +29,8 @@ const ( const consoleDevice string = "/dev/tty" -var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R") -var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R") +var offsetRegexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R") +var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R") func (r *LightRenderer) PassThrough(str string) { r.queued.WriteString("\x1b7" + str + "\x1b8") @@ -78,6 +80,7 @@ func (r *LightRenderer) flush() { // Light renderer type LightRenderer struct { + closed *util.AtomicBool theme *ColorTheme mouse bool forceBlack bool @@ -123,19 +126,24 @@ type LightWindow struct { 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{ + closed: util.NewAtomicBool(false), theme: theme, forceBlack: forceBlack, mouse: mouse, clearOnExit: clearOnExit, - ttyin: openTtyIn(), + ttyin: in, yoffset: 0, tabstop: tabstop, fullscreen: fullscreen, upOneLine: false, maxHeightFunc: maxHeightFunc} - return &r + return &r, nil } func repeat(r rune, times int) string { @@ -153,11 +161,11 @@ func atoi(s string, defaultValue int) int { return value } -func (r *LightRenderer) Init() { +func (r *LightRenderer) Init() error { r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay) if err := r.initPlatform(); err != nil { - errorExit(err.Error()) + return err } r.updateTerminalSize() initTheme(r.theme, r.defaultTheme(), r.forceBlack) @@ -195,6 +203,7 @@ func (r *LightRenderer) Init() { if !r.fullscreen && r.mouse { r.yoffset, _ = r.findOffset() } + return nil } func (r *LightRenderer) Resize(maxHeightFunc func(int) int) { @@ -233,15 +242,16 @@ func getEnv(name string, defaultValue int) int { return atoi(env, defaultValue) } -func (r *LightRenderer) getBytes() []byte { - return r.getBytesInternal(r.buffer, false) +func (r *LightRenderer) getBytes() ([]byte, error) { + 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) if !nonblock && !ok { r.Close() - errorExit("Failed to read " + consoleDevice) + return nil, errors.New("failed to read " + consoleDevice) } retries := 0 @@ -272,19 +282,23 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte { // so terminate fzf immediately. if len(buffer) > maxInputBuffer { 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 { + var err error 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 { - panic("Empty buffer") + return Event{Fatal, 0, nil} } sz := 1 @@ -315,7 +329,9 @@ func (r *LightRenderer) GetChar() Event { ev := r.escSequence(&sz) // Second chance 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) } return ev @@ -738,6 +754,7 @@ func (r *LightRenderer) Close() { r.flush() r.closePlatform() r.restoreTerminal() + r.closed.Set(true) } func (r *LightRenderer) Top() int { diff --git a/src/tui/light_unix.go b/src/tui/light_unix.go index 55e2b24..a5499a0 100644 --- a/src/tui/light_unix.go +++ b/src/tui/light_unix.go @@ -3,7 +3,7 @@ package tui import ( - "fmt" + "errors" "os" "os/exec" "strings" @@ -48,19 +48,18 @@ func (r *LightRenderer) closePlatform() { // NOOP } -func openTtyIn() *os.File { +func openTtyIn() (*os.File, error) { in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) if err != nil { tty := ttyname() if len(tty) > 0 { 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) - util.Exit(2) + return nil, errors.New("failed to open " + consoleDevice) } - return in + return in, nil } func (r *LightRenderer) setupTerminal() { @@ -86,9 +85,14 @@ func (r *LightRenderer) updateTerminalSize() { func (r *LightRenderer) findOffset() (row int, col int) { r.csi("6n") r.flush() + var err error bytes := []byte{} 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) if len(offsets) > 3 { // Add anything we skipped over to the input buffer diff --git a/src/tui/light_windows.go b/src/tui/light_windows.go index 62b10c1..635b892 100644 --- a/src/tui/light_windows.go +++ b/src/tui/light_windows.go @@ -72,7 +72,7 @@ func (r *LightRenderer) initPlatform() error { go func() { fd := int(r.inHandle) b := make([]byte, 1) - for { + for !r.closed.Get() { // HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT. _ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput) @@ -91,9 +91,9 @@ func (r *LightRenderer) closePlatform() { windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput) } -func openTtyIn() *os.File { +func openTtyIn() (*os.File, error) { // not used - return nil + return nil, nil } func (r *LightRenderer) setupTerminal() error { diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 9b8f862..16ce452 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -7,7 +7,6 @@ import ( "time" "github.com/gdamore/tcell/v2" - "github.com/gdamore/tcell/v2/encoding" "github.com/junegunn/fzf/src/util" "github.com/rivo/uniseg" @@ -146,13 +145,13 @@ var ( _initialResize bool = true ) -func (r *FullscreenRenderer) initScreen() { +func (r *FullscreenRenderer) initScreen() error { s, e := tcell.NewScreen() if e != nil { - errorExit(e.Error()) + return e } if e = s.Init(); e != nil { - errorExit(e.Error()) + return e } if r.mouse { s.EnableMouse() @@ -160,16 +159,21 @@ func (r *FullscreenRenderer) initScreen() { s.DisableMouse() } _screen = s + + return nil } -func (r *FullscreenRenderer) Init() { +func (r *FullscreenRenderer) Init() error { if os.Getenv("TERM") == "cygwin" { os.Setenv("TERM", "") } - encoding.Register() - r.initScreen() + if err := r.initScreen(); err != nil { + return err + } initTheme(r.theme, r.defaultTheme(), r.forceBlack) + + return nil } func (r *FullscreenRenderer) Top() int { diff --git a/src/tui/tui.go b/src/tui/tui.go index a56edc7..e4858c6 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -1,8 +1,6 @@ package tui import ( - "fmt" - "os" "strconv" "time" @@ -104,6 +102,7 @@ const ( CtrlAlt Invalid + Fatal Mouse DoubleClick @@ -525,7 +524,7 @@ type TermSize struct { } type Renderer interface { - Init() + Init() error Resize(maxHeightFunc func(int) int) Pause(clear 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() { Default16 = &ColorTheme{ Colored: true, diff --git a/src/util/atexit.go b/src/util/atexit.go index a22a3a9..6212378 100644 --- a/src/util/atexit.go +++ b/src/util/atexit.go @@ -1,7 +1,6 @@ package util import ( - "os" "sync" ) @@ -25,14 +24,5 @@ func RunAtExitFuncs() { for i := len(fns) - 1; i >= 0; i-- { fns[i]() } -} - -// 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() + atExitFuncs = nil } diff --git a/src/util/util_unix.go b/src/util/util_unix.go index 4410a9b..5a67066 100644 --- a/src/util/util_unix.go +++ b/src/util/util_unix.go @@ -61,7 +61,7 @@ func (x *Executor) Become(stdin *os.File, environ []string, command string) { shellPath, err := exec.LookPath(x.shell) if err != nil { fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error()) - Exit(127) + os.Exit(127) } args := append([]string{shellPath}, append(x.args, command)...) SetStdin(stdin) diff --git a/src/util/util_windows.go b/src/util/util_windows.go index 7bbf6ee..f29e33b 100644 --- a/src/util/util_windows.go +++ b/src/util/util_windows.go @@ -97,15 +97,15 @@ func (x *Executor) Become(stdin *os.File, environ []string, command string) { err := cmd.Start() if err != nil { fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error()) - Exit(127) + os.Exit(127) } err = cmd.Wait() if err != nil { if exitError, ok := err.(*exec.ExitError); ok { - Exit(exitError.ExitCode()) + os.Exit(exitError.ExitCode()) } } - Exit(0) + os.Exit(0) } func escapeArg(s string) string { diff --git a/test/test_go.rb b/test/test_go.rb index d3e6b47..5563c08 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -557,7 +557,7 @@ class TestGoFZF < TestBase def test_expect 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.send_keys '55' tmux.until { |lines| assert_equal ' 1/100', lines[-2] }