From d2554e2dbe4e2f4a64ad7613858754afd7bb73cd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 5 May 2024 02:35:36 +0900 Subject: [PATCH] Refactor the code so that fzf can be used as a library --- main.go | 20 +- main_test.go | 1 + src/actiontype_string.go | 167 +++---- src/constants.go | 10 +- src/core.go | 31 +- src/matcher.go | 15 +- src/options.go | 910 ++++++++++++++++++++++++------------ src/options_no_pprof.go | 4 +- src/options_test.go | 54 +-- src/server.go | 24 +- src/terminal.go | 170 ++++--- src/tokenizer_test.go | 9 +- src/tui/dummy.go | 2 +- src/tui/eventtype_string.go | 51 +- src/tui/light.go | 47 +- 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 | 1 + 20 files changed, 999 insertions(+), 569 deletions(-) diff --git a/main.go b/main.go index 768148ef..690f422f 100644 --- a/main.go +++ b/main.go @@ -3,10 +3,12 @@ package main import ( _ "embed" "fmt" + "os" "strings" fzf "github.com/junegunn/fzf/src" "github.com/junegunn/fzf/src/protector" + "github.com/junegunn/fzf/src/util" ) var version string = "0.51" @@ -33,9 +35,19 @@ func printScript(label string, content []byte) { fmt.Println("### end: " + label + " ###") } +func errorExit(msg string) { + os.Stderr.WriteString(msg + "\n") + os.Exit(fzf.ExitError) +} + func main() { protector.Protect() - options := fzf.ParseOptions() + + options, err := fzf.ParseOptions(true, os.Args[1:]) + if err != nil { + errorExit(err.Error()) + return + } if options.Bash { printScript("key-bindings.bash", bashKeyBindings) printScript("completion.bash", bashCompletion) @@ -51,5 +63,9 @@ func main() { fmt.Println("fzf_key_bindings") return } - fzf.Run(options, version, revision) + code, err := fzf.Run(options, version, revision) + if err != nil { + os.Stderr.WriteString(err.Error() + "\n") + } + util.Exit(code) } diff --git a/main_test.go b/main_test.go index 763b2821..783f9e02 100644 --- a/main_test.go +++ b/main_test.go @@ -143,6 +143,7 @@ func TestOSExitNotAllowed(t *testing.T) { t.Skip("skipping: short test") } allowed := map[string]int{ + "main.go": 1, // os.Exit allowed 1 time in "main.go" "src/util/atexit.go": 1, // os.Exit allowed 1 time in "atexit.go" } var errOsExit bool diff --git a/src/actiontype_string.go b/src/actiontype_string.go index a9d931d7..6b07134b 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/constants.go b/src/constants.go index faf6a0e5..dd2e870e 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 14aa781f..f85628d0 100644 --- a/src/core.go +++ b/src/core.go @@ -28,7 +28,7 @@ func sbytes(data string) []byte { } // Run starts fzf -func Run(opts *Options, version string, revision string) { +func Run(opts *Options, version string, revision string) (int, error) { defer util.RunAtExitFuncs() sort := opts.Sort > 0 @@ -40,7 +40,7 @@ func Run(opts *Options, version string, revision string) { } else { fmt.Println(version) } - util.Exit(exitOk) + return ExitOk, nil } // Event channel @@ -197,9 +197,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 +210,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,7 +262,10 @@ func Run(opts *Options, version string, revision string) { header = make([]string, 0, opts.HeaderLines) go reader.restart(command, environ) } - for { + + exitCode := ExitOk + running := true + for running { delay := true ticks++ input := func() []rune { @@ -278,7 +285,9 @@ func Run(opts *Options, version string, revision string) { if reading { reader.terminate() } - util.Exit(value.(int)) + exitCode = value.(int) + running = false + return case EvtReadNew, EvtReadFin: if evt == EvtReadFin && nextCommand != nil { restart(*nextCommand, nextEnviron) @@ -378,10 +387,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) + running = false + return } determine(val.final) } @@ -399,4 +409,5 @@ func Run(opts *Options, version string, revision string) { time.Sleep(dur) } } + return exitCode, nil } diff --git a/src/matcher.go b/src/matcher.go index b9288bb6..73ceebbf 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -35,6 +35,7 @@ type Matcher struct { const ( reqRetry util.EventType = iota reqReset + reqStop ) // NewMatcher returns a new Matcher @@ -60,8 +61,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 == reqStop { + stop = true + return + } switch val := val.(type) { case MatchRequest: request = val @@ -71,6 +77,9 @@ func (m *Matcher) Loop() { } events.Clear() }) + if stop { + break + } if request.sort != m.sort || request.revision != m.revision { m.sort = request.sort @@ -236,3 +245,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(reqStop, MatchRequest{}) +} diff --git a/src/options.go b/src/options.go index 5098318d..ae13669f 100644 --- a/src/options.go +++ b/src/options.go @@ -1,6 +1,7 @@ package fzf import ( + "errors" "fmt" "os" "regexp" @@ -247,9 +248,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 +261,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 { @@ -463,13 +466,6 @@ func help(code int) { 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 +475,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 +492,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 +545,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 +579,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 + } + if optional && str == "" { + return tui.DefaultBorderShape, nil } - return tui.BorderNone + 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,36 +810,40 @@ 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") @@ -851,13 +855,13 @@ func parseTiebreak(str string) []criterion { check(&hasEnd, "end") 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 +869,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 +885,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 +1017,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 +1035,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 ( @@ -1120,13 +1126,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 +1309,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 +1319,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 +1338,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 +1367,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 +1377,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 +1455,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 +1512,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", " ") - } + return infoHidden, "", nil + } + 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 } - errorExit("invalid info style (expected: default|right|hidden|inline[-right][:PREFIX])") } - return infoDefault, "" + 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 +1580,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 +1643,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,60 +1662,91 @@ 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 for i := 0; i < len(allArgs); i++ { @@ -1688,20 +1755,20 @@ func parseOptions(opts *Options, allArgs []string) { case "--bash": opts.Bash = true if opts.Zsh || opts.Fish { - errorExit("cannot specify --bash with --zsh or --fish") + return errors.New("cannot specify --bash with --zsh or --fish") } case "--zsh": opts.Zsh = true if opts.Bash || opts.Fish { - errorExit("cannot specify --zsh with --bash or --fish") + return errors.New("cannot specify --zsh with --bash or --fish") } case "--fish": opts.Fish = true if opts.Bash || opts.Zsh { - errorExit("cannot specify --fish with --bash or --zsh") + return errors.New("cannot specify --fish with --bash or --zsh") } case "-h", "--help": - help(exitOk) + help(ExitOk) case "-x", "--extended": opts.Extended = true case "-e", "--exact": @@ -1715,20 +1782,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 +1828,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 +1901,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 +1925,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 +1949,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 +1976,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 +1995,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 +2022,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 +2045,74 @@ 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")) + str, err := nextString(allArgs, &i, "number of header lines required") + if err != nil { + return err + } + opts.HeaderLines, err = atoi(str) + if 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 +2123,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 +2164,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 +2194,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 +2207,48 @@ 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"), ",")) + str, err := nextString(allArgs, &i, "directory names to ignore required") + if err != nil { + return err + } + opts.WalkerSkip = filterNonEmpty(strings.Split(str, ",")) case "--version": opts.Version = true 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 +2258,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,21 +2280,41 @@ 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) + opts.Nth, err = splitNth(value) + if err != nil { + return err + } } else if match, value := optString(arg, "--with-nth="); match { - opts.WithNth = splitNth(value) + opts.WithNth, err = splitNth(value) + if 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) + opts.Multi, err = atoi(value) + if err != nil { + return err + } } else if match, value := optString(arg, "--height="); match { - opts.Height = parseHeight(value) + opts.Height, err = parseHeight(value) + if err != nil { + return err + } } else if match, value := optString(arg, "--min-height="); match { - opts.MinHeight = atoi(value) + opts.MinHeight, err = atoi(value) + if 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) + opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(value) + if err != nil { + return err + } } else if match, value := optString(arg, "--separator="); match { opts.Separator = &value } else if match, value := optString(arg, "--scrollbar="); match { @@ -2046,23 +2322,41 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, value := optString(arg, "--toggle-sort="); match { parseToggleSort(opts.Keymap, value) } 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) } 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 { @@ -2070,73 +2364,86 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, value := optString(arg, "--preview-window="); match { parsePreviewWindow(&opts.Preview, value) } 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 +2456,31 @@ func validateSign(sign string, signOptName string) error { return nil } -func postProcessOptions(opts *Options) { +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") + 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 +2534,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 +2550,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 +2572,70 @@ func postProcessOptions(opts *Options) { theme.Spinner = boldify(theme.Spinner) } - processScheme(opts) + return processScheme(opts) } func expectsArbitraryString(opt string) bool { switch opt { - case "-q", "--query", "-f", "--filter", "--header", "--prompt": + case "-q", "--query", "-f", "--filter", "--header", "--prompt", + "--border-label", "--preview-label", "--separator", "--ellipsis": // Seriously? return true } return false } // 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])) { + for idx, arg := range args { + if arg == "--version" && (idx == 0 || idx > 0 && !expectsArbitraryString(args[idx-1])) { opts.Version = true - return opts + return opts, nil } } - // 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()) + 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()) + } + + 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 := parseOptions(opts, args); err != nil { + return nil, err + } if err := opts.initProfiling(); err != nil { - errorExit("failed to start pprof profiles: " + err.Error()) + return nil, errors.New("failed to start pprof profiles: " + err.Error()) } - postProcessOptions(opts) + err := postProcessOptions(opts) - return opts + return opts, err } diff --git a/src/options_no_pprof.go b/src/options_no_pprof.go index 1a19bc63..1093fc10 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 8e3b20f4..270af5c8 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/server.go b/src/server.go index aa80eb4f..42cb9854 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,7 +109,7 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon conn, err := listener.Accept() if err != nil { if errors.Is(err, net.ErrClosed) { - break + return } else { continue } @@ -117,10 +117,9 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon 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 +216,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 951b8c3b..2a8192b9 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,6 +51,7 @@ 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 @@ -63,6 +66,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 +245,7 @@ type Terminal struct { unicode bool listenAddr *listenAddress listenPort *int + listener net.Listener listenUnsafe bool borderShape tui.BorderShape cleanExit bool @@ -259,7 +264,7 @@ type Terminal struct { hasResizeActions bool triggerLoad bool reading bool - running bool + running *util.AtomicBool failed *string jumping jumpMode jumpLabels string @@ -278,12 +283,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 +345,7 @@ const ( reqPreviewRefresh reqPreviewDelayed reqQuit + reqFatal ) type action struct { @@ -380,6 +386,7 @@ const ( actDeleteChar actDeleteCharEof actEndOfLine + actFatal actForwardChar actForwardWord actKillLine @@ -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 { @@ -2512,21 +2526,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 +2856,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 +2891,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 +2915,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 +2940,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 +2954,21 @@ 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() + return err + } t.termSize = t.tui.Size() t.resizeWindows(false) t.window.Erase() @@ -2945,7 +2985,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() @@ -3071,12 +3111,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 @@ -3131,11 +3172,14 @@ func (t *Terminal) Loop() { var focusedIndex int32 = minItem.Index() var version int64 = -1 running := true - code := exitError + code := ExitError exit := func(getCode func() int) { + 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 +3247,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 +3277,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 +3292,11 @@ func (t *Terminal) Loop() { t.mutex.Unlock() }) } - // prof.Stop() - t.killPreview(code) + + t.eventBox.Set(EvtQuit, code) + t.running.Set(false) + t.killPreview() + cancel() }() looping := true @@ -3256,8 +3306,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 +3411,7 @@ func (t *Terminal) Loop() { t.pressed = ret t.reqBox.Set(reqClose, nil) t.mutex.Unlock() - return + return nil } } @@ -3547,8 +3605,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 +3639,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: @@ -4066,15 +4127,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 +4284,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 985cef9f..3119f797 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 7760a724..3456907f 100644 --- a/src/tui/dummy.go +++ b/src/tui/dummy.go @@ -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 ce34d36e..d752163d 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 a045b783..34430e42 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" @@ -26,6 +28,7 @@ const ( ) const consoleDevice string = "/dev/tty" +const fatalError string = "Failed to read " + consoleDevice var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R") var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R") @@ -78,6 +81,7 @@ func (r *LightRenderer) flush() { // Light renderer type LightRenderer struct { + closed *util.AtomicBool theme *ColorTheme mouse bool forceBlack bool @@ -123,19 +127,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 +162,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 +204,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 +243,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 +283,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 +330,10 @@ func (r *LightRenderer) GetChar() Event { ev := r.escSequence(&sz) // Second chance if ev.Type == Invalid { - r.buffer = r.getBytes() + r.buffer, err = r.getBytes() + if err != nil { + return Event{Fatal, 0, nil} + } ev = r.escSequence(&sz) } return ev @@ -738,6 +756,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 55e2b246..f8cb32af 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 62b10c12..635b8926 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 9b8f8620..16ce452d 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 a56edc7f..e4858c66 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 a22a3a96..a1844517 100644 --- a/src/util/atexit.go +++ b/src/util/atexit.go @@ -25,6 +25,7 @@ func RunAtExitFuncs() { for i := len(fns) - 1; i >= 0; i-- { fns[i]() } + atExitFuncs = nil } // Exit executes any functions registered with AtExit() then exits the program