Repository: junegunn/fzf
Pull request #3769 (2024-05-04 17:36:18)

Refactor the code so that fzf can be used as a library
- [x] Remove `exit`s
- [x] Terminate goroutines
- [x] Remove/minimize reliance on global states (e.g. cache)


Patch:
diff --git a/Makefile b/Makefile
index 16150e5a..f4abe6e6 100644
--- a/Makefile
+++ b/Makefile
@@ -79,7 +79,6 @@ all: target/$(BINARY)
 test: $(SOURCES)
 	[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
 	SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
-				github.com/junegunn/fzf \
 				github.com/junegunn/fzf/src \
 				github.com/junegunn/fzf/src/algo \
 				github.com/junegunn/fzf/src/tui \
diff --git a/main.go b/main.go
index 768148ef..f0074b3e 100644
--- a/main.go
+++ b/main.go
@@ -3,14 +3,15 @@ package main
 import (
 	_ "embed"
 	"fmt"
+	"os"
 	"strings"
 
 	fzf "github.com/junegunn/fzf/src"
 	"github.com/junegunn/fzf/src/protector"
 )
 
-var version string = "0.51"
-var revision string = "devel"
+var version = "0.51"
+var revision = "devel"
 
 //go:embed shell/key-bindings.bash
 var bashKeyBindings []byte
@@ -33,9 +34,21 @@ func printScript(label string, content []byte) {
 	fmt.Println("### end: " + label + " ###")
 }
 
+func exit(code int, err error) {
+	if err != nil {
+		os.Stderr.WriteString(err.Error() + "\n")
+	}
+	os.Exit(code)
+}
+
 func main() {
 	protector.Protect()
-	options := fzf.ParseOptions()
+
+	options, err := fzf.ParseOptions(true, os.Args[1:])
+	if err != nil {
+		exit(fzf.ExitError, err)
+		return
+	}
 	if options.Bash {
 		printScript("key-bindings.bash", bashKeyBindings)
 		printScript("completion.bash", bashCompletion)
@@ -51,5 +64,19 @@ func main() {
 		fmt.Println("fzf_key_bindings")
 		return
 	}
-	fzf.Run(options, version, revision)
+	if options.Help {
+		fmt.Print(fzf.Usage)
+		return
+	}
+	if options.Version {
+		if len(revision) > 0 {
+			fmt.Printf("%s (%s)\n", version, revision)
+		} else {
+			fmt.Println(version)
+		}
+		return
+	}
+
+	code, err := fzf.Run(options)
+	exit(code, err)
 }
diff --git a/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/algo/algo.go b/src/algo/algo.go
index 3e689746..c85ec82e 100644
--- a/src/algo/algo.go
+++ b/src/algo/algo.go
@@ -152,7 +152,7 @@ var (
 	// Extra bonus for word boundary after slash, colon, semi-colon, and comma
 	bonusBoundaryDelimiter int16 = bonusBoundary + 1
 
-	initialCharClass charClass = charWhite
+	initialCharClass = charWhite
 
 	// A minor optimization that can give 15%+ performance boost
 	asciiCharClasses [unicode.MaxASCII + 1]charClass
diff --git a/src/algo/normalize.go b/src/algo/normalize.go
index 93247908..25e92983 100644
--- a/src/algo/normalize.go
+++ b/src/algo/normalize.go
@@ -3,7 +3,7 @@
 
 package algo
 
-var normalized map[rune]rune = map[rune]rune{
+var normalized = map[rune]rune{
 	0x00E1: 'a', //  WITH ACUTE, LATIN SMALL LETTER
 	0x0103: 'a', //  WITH BREVE, LATIN SMALL LETTER
 	0x01CE: 'a', //  WITH CARON, LATIN SMALL LETTER
diff --git a/src/ansi.go b/src/ansi.go
index 53fae954..dbe4fd66 100644
--- a/src/ansi.go
+++ b/src/ansi.go
@@ -292,7 +292,7 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
 
 func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
 	var remaining string
-	i := -1
+	var i int
 	if delimiter == 0 {
 		// Faster than strings.IndexAny(";:")
 		i = strings.IndexByte(s, ';')
@@ -350,7 +350,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
 	state256 := 0
 	ptr := &state.fg
 
-	var delimiter byte = 0
+	var delimiter byte
 	count := 0
 	for len(ansiCode) != 0 {
 		var num int
diff --git a/src/cache.go b/src/cache.go
index df1a6ab7..39d4250d 100644
--- a/src/cache.go
+++ b/src/cache.go
@@ -12,8 +12,14 @@ type ChunkCache struct {
 }
 
 // NewChunkCache returns a new ChunkCache
-func NewChunkCache() ChunkCache {
-	return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
+func NewChunkCache() *ChunkCache {
+	return &ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
+}
+
+func (cc *ChunkCache) Clear() {
+	cc.mutex.Lock()
+	cc.cache = make(map[*Chunk]*queryCache)
+	cc.mutex.Unlock()
 }
 
 // Add adds the list to the cache
diff --git a/src/constants.go b/src/constants.go
index 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..dbae6a69 100644
--- a/src/core.go
+++ b/src/core.go
@@ -2,7 +2,6 @@
 package fzf
 
 import (
-	"fmt"
 	"sync"
 	"time"
 	"unsafe"
@@ -27,22 +26,29 @@ func sbytes(data string) []byte {
 	return unsafe.Slice(unsafe.StringData(data), len(data))
 }
 
+type quitSignal struct {
+	code int
+	err  error
+}
+
 // Run starts fzf
-func Run(opts *Options, version string, revision string) {
-	defer util.RunAtExitFuncs()
+func Run(opts *Options) (int, error) {
+	if err := postProcessOptions(opts); err != nil {
+		return ExitError, err
+	}
 
-	sort := opts.Sort > 0
-	sortCriteria = opts.Criteria
+	defer util.RunAtExitFuncs()
 
-	if opts.Version {
-		if len(revision) > 0 {
-			fmt.Printf("%s (%s)\n", version, revision)
-		} else {
-			fmt.Println(version)
+	// Output channel given
+	if opts.Output != nil {
+		opts.Printer = func(str string) {
+			opts.Output <- str
 		}
-		util.Exit(exitOk)
 	}
 
+	sort := opts.Sort > 0
+	sortCriteria = opts.Criteria
+
 	// Event channel
 	eventBox := util.NewEventBox()
 
@@ -131,7 +137,7 @@ func Run(opts *Options, version string, revision string) {
 		reader = NewReader(func(data []byte) bool {
 			return chunkList.Push(data)
 		}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
-		go reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
+		go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
 	}
 
 	// Matcher
@@ -147,14 +153,16 @@ func Run(opts *Options, version string, revision string) {
 			forward = true
 		}
 	}
+	cache := NewChunkCache()
+	patternCache := make(map[string]*Pattern)
 	patternBuilder := func(runes []rune) *Pattern {
-		return BuildPattern(
+		return BuildPattern(cache, patternCache,
 			opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
 			opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
 	}
 	inputRevision := 0
 	snapshotRevision := 0
-	matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox, inputRevision)
+	matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
 
 	// Filtering mode
 	if opts.Filter != nil {
@@ -182,7 +190,7 @@ func Run(opts *Options, version string, revision string) {
 					}
 					return false
 				}, eventBox, executor, opts.ReadZero, false)
-			reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
+			reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
 		} else {
 			eventBox.Unwatch(EvtReadNew)
 			eventBox.WaitFor(EvtReadFin)
@@ -197,9 +205,9 @@ func Run(opts *Options, version string, revision string) {
 			}
 		}
 		if found {
-			util.Exit(exitOk)
+			return ExitOk, nil
 		}
-		util.Exit(exitNoMatch)
+		return ExitNoMatch, nil
 	}
 
 	// Synchronous search
@@ -210,9 +218,13 @@ func Run(opts *Options, version string, revision string) {
 
 	// Go interactive
 	go matcher.Loop()
+	defer matcher.Stop()
 
 	// Terminal I/O
-	terminal := NewTerminal(opts, eventBox, executor)
+	terminal, err := NewTerminal(opts, eventBox, executor)
+	if err != nil {
+		return ExitError, err
+	}
 	maxFit := 0 // Maximum number of items that can fit on screen
 	padHeight := 0
 	heightUnknown := opts.Height.auto
@@ -258,6 +270,9 @@ func Run(opts *Options, version string, revision string) {
 		header = make([]string, 0, opts.HeaderLines)
 		go reader.restart(command, environ)
 	}
+
+	exitCode := ExitOk
+	stop := false
 	for {
 		delay := true
 		ticks++
@@ -278,7 +293,11 @@ func Run(opts *Options, version string, revision string) {
 					if reading {
 						reader.terminate()
 					}
-					util.Exit(value.(int))
+					quitSignal := value.(quitSignal)
+					exitCode = quitSignal.code
+					err = quitSignal.err
+					stop = true
+					return
 				case EvtReadNew, EvtReadFin:
 					if evt == EvtReadFin && nextCommand != nil {
 						restart(*nextCommand, nextEnviron)
@@ -378,10 +397,11 @@ func Run(opts *Options, version string, revision string) {
 									for i := 0; i < count; i++ {
 										opts.Printer(val.Get(i).item.AsString(opts.Ansi))
 									}
-									if count > 0 {
-										util.Exit(exitOk)
+									if count == 0 {
+										exitCode = ExitNoMatch
 									}
-									util.Exit(exitNoMatch)
+									stop = true
+									return
 								}
 								determine(val.final)
 							}
@@ -392,6 +412,9 @@ func Run(opts *Options, version string, revision string) {
 			}
 			events.Clear()
 		})
+		if stop {
+			break
+		}
 		if delay && reading {
 			dur := util.DurWithin(
 				time.Duration(ticks)*coordinatorDelayStep,
@@ -399,4 +422,5 @@ func Run(opts *Options, version string, revision string) {
 			time.Sleep(dur)
 		}
 	}
+	return exitCode, err
 }
diff --git a/src/matcher.go b/src/matcher.go
index b9288bb6..26426e4f 100644
--- a/src/matcher.go
+++ b/src/matcher.go
@@ -21,6 +21,7 @@ type MatchRequest struct {
 
 // Matcher is responsible for performing search
 type Matcher struct {
+	cache          *ChunkCache
 	patternBuilder func([]rune) *Pattern
 	sort           bool
 	tac            bool
@@ -38,10 +39,11 @@ const (
 )
 
 // NewMatcher returns a new Matcher
-func NewMatcher(patternBuilder func([]rune) *Pattern,
+func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
 	sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher {
 	partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
 	return &Matcher{
+		cache:          cache,
 		patternBuilder: patternBuilder,
 		sort:           sort,
 		tac:            tac,
@@ -60,8 +62,13 @@ func (m *Matcher) Loop() {
 	for {
 		var request MatchRequest
 
+		stop := false
 		m.reqBox.Wait(func(events *util.Events) {
-			for _, val := range *events {
+			for t, val := range *events {
+				if t == reqQuit {
+					stop = true
+					return
+				}
 				switch val := val.(type) {
 				case MatchRequest:
 					request = val
@@ -71,12 +78,15 @@ func (m *Matcher) Loop() {
 			}
 			events.Clear()
 		})
+		if stop {
+			break
+		}
 
 		if request.sort != m.sort || request.revision != m.revision {
 			m.sort = request.sort
 			m.revision = request.revision
 			m.mergerCache = make(map[string]*Merger)
-			clearChunkCache()
+			m.cache.Clear()
 		}
 
 		// Restart search
@@ -236,3 +246,7 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
 	}
 	m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
 }
+
+func (m *Matcher) Stop() {
+	m.reqBox.Set(reqQuit, nil)
+}
diff --git a/src/options.go b/src/options.go
index 5098318d..9abecc3b 100644
--- a/src/options.go
+++ b/src/options.go
@@ -1,6 +1,7 @@
 package fzf
 
 import (
+	"errors"
 	"fmt"
 	"os"
 	"regexp"
@@ -10,13 +11,12 @@ import (
 
 	"github.com/junegunn/fzf/src/algo"
 	"github.com/junegunn/fzf/src/tui"
-	"github.com/junegunn/fzf/src/util"
 
 	"github.com/mattn/go-shellwords"
 	"github.com/rivo/uniseg"
 )
 
-const usage = `usage: fzf [options]
+const Usage = `usage: fzf [options]
 
   Search
     -x, --extended         Extended-search mode
@@ -247,9 +247,10 @@ func (o *previewOpts) Toggle() {
 	o.hidden = !o.hidden
 }
 
-func parseLabelPosition(opts *labelOpts, arg string) {
+func parseLabelPosition(opts *labelOpts, arg string) error {
 	opts.column = 0
 	opts.bottom = false
+	var err error
 	for _, token := range splitRegexp.Split(strings.ToLower(arg), -1) {
 		switch token {
 		case "center":
@@ -259,9 +260,10 @@ func parseLabelPosition(opts *labelOpts, arg string) {
 		case "top":
 			opts.bottom = false
 		default:
-			opts.column = atoi(token)
+			opts.column, err = atoi(token)
 		}
 	}
+	return err
 }
 
 func (a previewOpts) aboveOrBelow() bool {
@@ -291,6 +293,8 @@ type walkerOpts struct {
 
 // Options stores the values of command-line options
 type Options struct {
+	Input        chan string
+	Output       chan string
 	Bash         bool
 	Zsh          bool
 	Fish         bool
@@ -365,6 +369,7 @@ type Options struct {
 	WalkerRoot   string
 	WalkerSkip   []string
 	Version      bool
+	Help         bool
 	CPUProfile   string
 	MEMProfile   string
 	BlockProfile string
@@ -455,21 +460,10 @@ func defaultOptions() *Options {
 		WalkerOpts:   walkerOpts{file: true, hidden: true, follow: true},
 		WalkerRoot:   ".",
 		WalkerSkip:   []string{".git", "node_modules"},
+		Help:         false,
 		Version:      false}
 }
 
-func help(code int) {
-	os.Stdout.WriteString(usage)
-	util.Exit(code)
-}
-
-var errorContext = ""
-
-func errorExit(msg string) {
-	os.Stderr.WriteString(errorContext + msg + "\n")
-	util.Exit(exitError)
-}
-
 func optString(arg string, prefixes ...string) (bool, string) {
 	for _, prefix := range prefixes {
 		if strings.HasPrefix(arg, prefix) {
@@ -479,13 +473,13 @@ func optString(arg string, prefixes ...string) (bool, string) {
 	return false, ""
 }
 
-func nextString(args []string, i *int, message string) string {
+func nextString(args []string, i *int, message string) (string, error) {
 	if len(args) > *i+1 {
 		*i++
 	} else {
-		errorExit(message)
+		return "", errors.New(message)
 	}
-	return args[*i]
+	return args[*i], nil
 }
 
 func optionalNextString(args []string, i *int) (bool, string) {
@@ -496,44 +490,52 @@ func optionalNextString(args []string, i *int) (bool, string) {
 	return false, ""
 }
 
-func atoi(str string) int {
+func atoi(str string) (int, error) {
 	num, err := strconv.Atoi(str)
 	if err != nil {
-		errorExit("not a valid integer: " + str)
+		return 0, errors.New("not a valid integer: " + str)
 	}
-	return num
+	return num, nil
 }
 
-func atof(str string) float64 {
+func atof(str string) (float64, error) {
 	num, err := strconv.ParseFloat(str, 64)
 	if err != nil {
-		errorExit("not a valid number: " + str)
+		return 0, errors.New("not a valid number: " + str)
 	}
-	return num
+	return num, nil
 }
 
-func nextInt(args []string, i *int, message string) int {
+func nextInt(args []string, i *int, message string) (int, error) {
 	if len(args) > *i+1 {
 		*i++
 	} else {
-		errorExit(message)
+		return 0, errors.New(message)
+	}
+	n, err := atoi(args[*i])
+	if err != nil {
+		return 0, errors.New(message)
 	}
-	return atoi(args[*i])
+	return n, nil
 }
 
-func optionalNumeric(args []string, i *int, defaultValue int) int {
+func optionalNumeric(args []string, i *int, defaultValue int) (int, error) {
 	if len(args) > *i+1 {
 		if strings.IndexAny(args[*i+1], "0123456789") == 0 {
 			*i++
-			return atoi(args[*i])
+			n, err := atoi(args[*i])
+			if err != nil {
+				return 0, err
+			}
+			return n, nil
 		}
 	}
-	return defaultValue
+	return defaultValue, nil
 }
 
-func splitNth(str string) []Range {
+func splitNth(str string) ([]Range, error) {
 	if match, _ := regexp.MatchString("^[0-9,-.]+$", str); !match {
-		errorExit("invalid format: " + str)
+		return nil, errors.New("invalid format: " + str)
 	}
 
 	tokens := strings.Split(str, ",")
@@ -541,11 +543,11 @@ func splitNth(str string) []Range {
 	for idx, s := range tokens {
 		r, ok := ParseRange(&s)
 		if !ok {
-			errorExit("invalid format: " + str)
+			return nil, errors.New("invalid format: " + str)
 		}
 		ranges[idx] = r
 	}
-	return ranges
+	return ranges, nil
 }
 
 func delimiterRegexp(str string) Delimiter {
@@ -575,72 +577,68 @@ func isNumeric(char uint8) bool {
 	return char >= '0' && char <= '9'
 }
 
-func parseAlgo(str string) algo.Algo {
+func parseAlgo(str string) (algo.Algo, error) {
 	switch str {
 	case "v1":
-		return algo.FuzzyMatchV1
+		return algo.FuzzyMatchV1, nil
 	case "v2":
-		return algo.FuzzyMatchV2
-	default:
-		errorExit("invalid algorithm (expected: v1 or v2)")
+		return algo.FuzzyMatchV2, nil
 	}
-	return algo.FuzzyMatchV2
+	return nil, errors.New("invalid algorithm (expected: v1 or v2)")
 }
 
-func processScheme(opts *Options) {
+func processScheme(opts *Options) error {
 	if !algo.Init(opts.Scheme) {
-		errorExit("invalid scoring scheme (expected: default|path|history)")
+		return errors.New("invalid scoring scheme (expected: default|path|history)")
 	}
 	if opts.Scheme == "history" {
 		opts.Criteria = []criterion{byScore}
 	}
+	return nil
 }
 
-func parseBorder(str string, optional bool) tui.BorderShape {
+func parseBorder(str string, optional bool) (tui.BorderShape, error) {
 	switch str {
 	case "rounded":
-		return tui.BorderRounded
+		return tui.BorderRounded, nil
 	case "sharp":
-		return tui.BorderSharp
+		return tui.BorderSharp, nil
 	case "bold":
-		return tui.BorderBold
+		return tui.BorderBold, nil
 	case "block":
-		return tui.BorderBlock
+		return tui.BorderBlock, nil
 	case "thinblock":
-		return tui.BorderThinBlock
+		return tui.BorderThinBlock, nil
 	case "double":
-		return tui.BorderDouble
+		return tui.BorderDouble, nil
 	case "horizontal":
-		return tui.BorderHorizontal
+		return tui.BorderHorizontal, nil
 	case "vertical":
-		return tui.BorderVertical
+		return tui.BorderVertical, nil
 	case "top":
-		return tui.BorderTop
+		return tui.BorderTop, nil
 	case "bottom":
-		return tui.BorderBottom
+		return tui.BorderBottom, nil
 	case "left":
-		return tui.BorderLeft
+		return tui.BorderLeft, nil
 	case "right":
-		return tui.BorderRight
+		return tui.BorderRight, nil
 	case "none":
-		return tui.BorderNone
-	default:
-		if optional && str == "" {
-			return tui.DefaultBorderShape
-		}
-		errorExit("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|none)")
+		return tui.BorderNone, nil
+	}
+	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,54 +808,64 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
 			} else if len(runes) == 1 {
 				chords[tui.Key(runes[0])] = key
 			} else {
-				exit("unsupported key: " + key)
-				return nil
+				return nil, errors.New("unsupported key: " + key)
 			}
 		}
 	}
-	return chords
+	return chords, nil
 }
 
-func parseTiebreak(str string) []criterion {
+func parseTiebreak(str string) ([]criterion, error) {
 	criteria := []criterion{byScore}
 	hasIndex := false
 	hasChunk := false
 	hasLength := false
 	hasBegin := false
 	hasEnd := false
-	check := func(notExpected *bool, name string) {
+	check := func(notExpected *bool, name string) error {
 		if *notExpected {
-			errorExit("duplicate sort criteria: " + name)
+			return errors.New("duplicate sort criteria: " + name)
 		}
 		if hasIndex {
-			errorExit("index should be the last criterion")
+			return errors.New("index should be the last criterion")
 		}
 		*notExpected = true
+		return nil
 	}
 	for _, str := range strings.Split(strings.ToLower(str), ",") {
 		switch str {
 		case "index":
-			check(&hasIndex, "index")
+			if err := check(&hasIndex, "index"); err != nil {
+				return nil, err
+			}
 		case "chunk":
-			check(&hasChunk, "chunk")
+			if err := check(&hasChunk, "chunk"); err != nil {
+				return nil, err
+			}
 			criteria = append(criteria, byChunk)
 		case "length":
-			check(&hasLength, "length")
+			if err := check(&hasLength, "length"); err != nil {
+				return nil, err
+			}
 			criteria = append(criteria, byLength)
 		case "begin":
-			check(&hasBegin, "begin")
+			if err := check(&hasBegin, "begin"); err != nil {
+				return nil, err
+			}
 			criteria = append(criteria, byBegin)
 		case "end":
-			check(&hasEnd, "end")
+			if err := check(&hasEnd, "end"); err != nil {
+				return nil, err
+			}
 			criteria = append(criteria, byEnd)
 		default:
-			errorExit("invalid sort criterion: " + str)
+			return nil, errors.New("invalid sort criterion: " + str)
 		}
 	}
 	if len(criteria) > 4 {
-		errorExit("at most 3 tiebreaks are allowed: " + str)
+		return nil, errors.New("at most 3 tiebreaks are allowed: " + str)
 	}
-	return criteria
+	return criteria, nil
 }
 
 func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme {
@@ -865,7 +873,8 @@ func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme {
 	return &dupe
 }
 
-func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
+func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, error) {
+	var err error
 	theme := dupeTheme(defaultTheme)
 	rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$")
 	for _, str := range strings.Split(strings.ToLower(str), ",") {
@@ -880,7 +889,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
 			theme = tui.NoColorTheme()
 		default:
 			fail := func() {
-				errorExit("invalid color specification: " + str)
+				// Let the code proceed to simplify the error handling
+				err = errors.New("invalid color specification: " + str)
 			}
 			// Color is disabled
 			if theme == nil {
@@ -1011,10 +1021,10 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
 			}
 		}
 	}
-	return theme
+	return theme, err
 }
 
-func parseWalkerOpts(str string) walkerOpts {
+func parseWalkerOpts(str string) (walkerOpts, error) {
 	opts := walkerOpts{}
 	for _, str := range strings.Split(strings.ToLower(str), ",") {
 		switch str {
@@ -1029,13 +1039,13 @@ func parseWalkerOpts(str string) walkerOpts {
 		case "":
 			// Ignored
 		default:
-			errorExit("invalid walker option: " + str)
+			return opts, errors.New("invalid walker option: " + str)
 		}
 	}
 	if !opts.file && !opts.dir {
-		errorExit("at least one of 'file' or 'dir' should be specified")
+		return opts, errors.New("at least one of 'file' or 'dir' should be specified")
 	}
-	return opts
+	return opts, nil
 }
 
 var (
@@ -1079,7 +1089,7 @@ Loop:
 			break
 		}
 		cs := string(action[0])
-		ce := ")"
+		var ce string
 		switch action[0] {
 		case ':':
 			masked += strings.Repeat(" ", len(action))
@@ -1120,13 +1130,13 @@ Loop:
 	return masked
 }
 
-func parseSingleActionList(str string, exit func(string)) []*action {
+func parseSingleActionList(str string) ([]*action, error) {
 	// We prepend a colon to satisfy executeRegexp and remove it later
 	masked := maskActionContents(":" + str)[1:]
-	return parseActionList(masked, str, []*action{}, false, exit)
+	return parseActionList(masked, str, []*action{}, false)
 }
 
-func parseActionList(masked string, original string, prevActions []*action, putAllowed bool, exit func(string)) []*action {
+func parseActionList(masked string, original string, prevActions []*action, putAllowed bool) ([]*action, error) {
 	maskedStrings := strings.Split(masked, "+")
 	originalStrings := make([]string, len(maskedStrings))
 	idx := 0
@@ -1303,7 +1313,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
 			if putAllowed {
 				appendAction(actChar)
 			} else {
-				exit("unable to put non-printable character")
+				return nil, errors.New("unable to put non-printable character")
 			}
 		default:
 			t := isExecuteAction(specLower)
@@ -1313,7 +1323,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
 				} else if specLower == "change-multi" {
 					appendAction(actChangeMulti)
 				} else {
-					exit("unknown action: " + spec)
+					return nil, errors.New("unknown action: " + spec)
 				}
 			} else {
 				offset := len(actionNameRegexp.FindString(spec))
@@ -1332,22 +1342,27 @@ func parseActionList(masked string, original string, prevActions []*action, putA
 				}
 				switch t {
 				case actUnbind, actRebind:
-					parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit)
+					if _, err := parseKeyChordsImpl(actionArg, spec[0:offset]+" target required"); err != nil {
+						return nil, err
+					}
 				case actChangePreviewWindow:
 					opts := previewOpts{}
 					for _, arg := range strings.Split(actionArg, "|") {
 						// Make sure that each expression is valid
-						parsePreviewWindowImpl(&opts, arg, exit)
+						if err := parsePreviewWindowImpl(&opts, arg); err != nil {
+							return nil, err
+						}
 					}
 				}
 			}
 		}
 		prevSpec = ""
 	}
-	return actions
+	return actions, nil
 }
 
-func parseKeymap(keymap map[tui.Event][]*action, str string, exit func(string)) {
+func parseKeymap(keymap map[tui.Event][]*action, str string) error {
+	var err error
 	masked := maskActionContents(str)
 	idx := 0
 	for _, pairStr := range strings.Split(masked, ",") {
@@ -1356,7 +1371,7 @@ func parseKeymap(keymap map[tui.Event][]*action, str string, exit func(string))
 
 		pair := strings.SplitN(pairStr, ":", 2)
 		if len(pair) < 2 {
-			exit("bind action not specified: " + origPairStr)
+			return errors.New("bind action not specified: " + origPairStr)
 		}
 		var key tui.Event
 		if len(pair[0]) == 1 && pair[0][0] == escapedColon {
@@ -1366,12 +1381,19 @@ func parseKeymap(keymap map[tui.Event][]*action, str string, exit func(string))
 		} else if len(pair[0]) == 1 && pair[0][0] == escapedPlus {
 			key = tui.Key('+')
 		} else {
-			keys := parseKeyChordsImpl(pair[0], "key name required", exit)
+			keys, err := parseKeyChordsImpl(pair[0], "key name required")
+			if err != nil {
+				return err
+			}
 			key = firstKey(keys)
 		}
 		putAllowed := key.Type == tui.Rune && unicode.IsGraphic(key.Char)
-		keymap[key] = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed, exit)
+		keymap[key], err = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed)
+		if err != nil {
+			return err
+		}
 	}
+	return nil
 }
 
 func isExecuteAction(str string) actionType {
@@ -1437,43 +1459,56 @@ func isExecuteAction(str string) actionType {
 	return actIgnore
 }
 
-func parseToggleSort(keymap map[tui.Event][]*action, str string) {
-	keys := parseKeyChords(str, "key name required")
+func parseToggleSort(keymap map[tui.Event][]*action, str string) error {
+	keys, err := parseKeyChords(str, "key name required")
+	if err != nil {
+		return err
+	}
 	if len(keys) != 1 {
-		errorExit("multiple keys specified")
+		return errors.New("multiple keys specified")
 	}
 	keymap[firstKey(keys)] = toActions(actToggleSort)
+	return nil
 }
 
 func strLines(str string) []string {
 	return strings.Split(strings.TrimSuffix(str, "\n"), "\n")
 }
 
-func parseSize(str string, maxPercent float64, label string) sizeSpec {
+func parseSize(str string, maxPercent float64, label string) (sizeSpec, error) {
+	var spec = sizeSpec{}
 	var val float64
+	var err error
 	percent := strings.HasSuffix(str, "%")
 	if percent {
-		val = atof(str[:len(str)-1])
+		if val, err = atof(str[:len(str)-1]); err != nil {
+			return spec, err
+		}
+
 		if val < 0 {
-			errorExit(label + " must be non-negative")
+			return spec, errors.New(label + " must be non-negative")
 		}
 		if val > maxPercent {
-			errorExit(fmt.Sprintf("%s too large (max: %d%%)", label, int(maxPercent)))
+			return spec, fmt.Errorf("%s too large (max: %d%%)", label, int(maxPercent))
 		}
 	} else {
 		if strings.Contains(str, ".") {
-			errorExit(label + " (without %) must be a non-negative integer")
+			return spec, errors.New(label + " (without %) must be a non-negative integer")
 		}
 
-		val = float64(atoi(str))
+		i, err := atoi(str)
+		if err != nil {
+			return spec, err
+		}
+		val = float64(i)
 		if val < 0 {
-			errorExit(label + " must be non-negative")
+			return spec, errors.New(label + " must be non-negative")
 		}
 	}
-	return sizeSpec{val, percent}
+	return sizeSpec{val, percent}, nil
 }
 
-func parseHeight(str string) heightSpec {
+func parseHeight(str string) (heightSpec, error) {
 	heightSpec := heightSpec{}
 	if strings.HasPrefix(str, "~") {
 		heightSpec.auto = true
@@ -1481,66 +1516,66 @@ func parseHeight(str string) heightSpec {
 	}
 	if strings.HasPrefix(str, "-") {
 		if heightSpec.auto {
-			errorExit("negative(-) height is not compatible with adaptive(~) height")
+			return heightSpec, errors.New("negative(-) height is not compatible with adaptive(~) height")
 		}
 		heightSpec.inverse = true
 		str = str[1:]
 	}
 
-	size := parseSize(str, 100, "height")
+	size, err := parseSize(str, 100, "height")
+	if err != nil {
+		return heightSpec, err
+	}
 	heightSpec.size = size.size
 	heightSpec.percent = size.percent
-	return heightSpec
+	return heightSpec, nil
 }
 
-func parseLayout(str string) layoutType {
+func parseLayout(str string) (layoutType, error) {
 	switch str {
 	case "default":
-		return layoutDefault
+		return layoutDefault, nil
 	case "reverse":
-		return layoutReverse
+		return layoutReverse, nil
 	case "reverse-list":
-		return layoutReverseList
-	default:
-		errorExit("invalid layout (expected: default / reverse / reverse-list)")
+		return layoutReverseList, nil
 	}
-	return layoutDefault
+	return layoutDefault, errors.New("invalid layout (expected: default / reverse / reverse-list)")
 }
 
-func parseInfoStyle(str string) (infoStyle, string) {
+func parseInfoStyle(str string) (infoStyle, string, error) {
 	switch str {
 	case "default":
-		return infoDefault, ""
+		return infoDefault, "", nil
 	case "right":
-		return infoRight, ""
+		return infoRight, "", nil
 	case "inline":
-		return infoInline, defaultInfoPrefix
+		return infoInline, defaultInfoPrefix, nil
 	case "inline-right":
-		return infoInlineRight, ""
+		return infoInlineRight, "", nil
 	case "hidden":
-		return infoHidden, ""
-	default:
-		type infoSpec struct {
-			name  string
-			style infoStyle
-		}
-		for _, spec := range []infoSpec{
-			{"inline", infoInline},
-			{"inline-right", infoInlineRight}} {
-			if strings.HasPrefix(str, spec.name+":") {
-				return spec.style, strings.ReplaceAll(str[len(spec.name)+1:], "\n", " ")
-			}
+		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 +1584,9 @@ func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string))
 	var alternative string
 	for _, match := range tokens {
 		if len(match[2]) > 0 {
-			opts.threshold = atoi(match[2])
+			if opts.threshold, err = atoi(match[2]); err != nil {
+				return err
+			}
 			alternative = match[3]
 			continue
 		}
@@ -1610,14 +1647,17 @@ func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string))
 			opts.follow = false
 		default:
 			if headerRegex.MatchString(token) {
-				opts.headerLines = atoi(token[1:])
+				if opts.headerLines, err = atoi(token[1:]); err != nil {
+					return err
+				}
 			} else if sizeRegex.MatchString(token) {
-				opts.size = parseSize(token, 99, "window size")
+				if opts.size, err = parseSize(token, 99, "window size"); err != nil {
+					return err
+				}
 			} else if offsetRegex.MatchString(token) {
 				opts.scroll = token
 			} else {
-				exit("invalid preview window option: " + token)
-				return
+				return errors.New("invalid preview window option: " + token)
 			}
 		}
 	}
@@ -1626,82 +1666,119 @@ func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string))
 		opts.alternative = &alternativeOpts
 		opts.alternative.hidden = false
 		opts.alternative.alternative = nil
-		parsePreviewWindowImpl(opts.alternative, alternative, exit)
+		err = parsePreviewWindowImpl(opts.alternative, alternative)
 	}
+	return err
 }
 
-func parseMargin(opt string, margin string) [4]sizeSpec {
+func parseMargin(opt string, margin string) ([4]sizeSpec, error) {
 	margins := strings.Split(margin, ",")
-	checked := func(str string) sizeSpec {
+	checked := func(str string) (sizeSpec, error) {
 		return parseSize(str, 49, opt)
 	}
 	switch len(margins) {
 	case 1:
-		m := checked(margins[0])
-		return [4]sizeSpec{m, m, m, m}
+		m, e := checked(margins[0])
+		return [4]sizeSpec{m, m, m, m}, e
 	case 2:
-		tb := checked(margins[0])
-		rl := checked(margins[1])
-		return [4]sizeSpec{tb, rl, tb, rl}
+		tb, e := checked(margins[0])
+		if e != nil {
+			return defaultMargin(), e
+		}
+		rl, e := checked(margins[1])
+		if e != nil {
+			return defaultMargin(), e
+		}
+		return [4]sizeSpec{tb, rl, tb, rl}, nil
 	case 3:
-		t := checked(margins[0])
-		rl := checked(margins[1])
-		b := checked(margins[2])
-		return [4]sizeSpec{t, rl, b, rl}
+		t, e := checked(margins[0])
+		if e != nil {
+			return defaultMargin(), e
+		}
+		rl, e := checked(margins[1])
+		if e != nil {
+			return defaultMargin(), e
+		}
+		b, e := checked(margins[2])
+		if e != nil {
+			return defaultMargin(), e
+		}
+		return [4]sizeSpec{t, rl, b, rl}, nil
 	case 4:
-		return [4]sizeSpec{
-			checked(margins[0]), checked(margins[1]),
-			checked(margins[2]), checked(margins[3])}
-	default:
-		errorExit("invalid " + opt + ": " + margin)
+		t, e := checked(margins[0])
+		if e != nil {
+			return defaultMargin(), e
+		}
+		r, e := checked(margins[1])
+		if e != nil {
+			return defaultMargin(), e
+		}
+		b, e := checked(margins[2])
+		if e != nil {
+			return defaultMargin(), e
+		}
+		l, e := checked(margins[3])
+		if e != nil {
+			return defaultMargin(), e
+		}
+		return [4]sizeSpec{t, r, b, l}, nil
 	}
-	return defaultMargin()
+	return [4]sizeSpec{}, errors.New("invalid " + opt + ": " + margin)
 }
 
-func parseOptions(opts *Options, allArgs []string) {
+func parseOptions(opts *Options, allArgs []string) error {
+	var err error
 	var historyMax int
 	if opts.History == nil {
 		historyMax = defaultHistoryMax
 	} else {
 		historyMax = opts.History.maxSize
 	}
-	setHistory := func(path string) {
+	setHistory := func(path string) error {
 		h, e := NewHistory(path, historyMax)
 		if e != nil {
-			errorExit(e.Error())
+			return e
 		}
 		opts.History = h
+		return nil
 	}
-	setHistoryMax := func(max int) {
+	setHistoryMax := func(max int) error {
 		historyMax = max
 		if historyMax < 1 {
-			errorExit("history max must be a positive integer")
+			return errors.New("history max must be a positive integer")
 		}
 		if opts.History != nil {
 			opts.History.maxSize = historyMax
 		}
+		return nil
 	}
 	validateJumpLabels := false
+	clearExitingOpts := func() {
+		// Last-one-wins strategy
+		opts.Bash = false
+		opts.Zsh = false
+		opts.Fish = false
+		opts.Help = false
+		opts.Version = false
+	}
 	for i := 0; i < len(allArgs); i++ {
 		arg := allArgs[i]
 		switch arg {
 		case "--bash":
+			clearExitingOpts()
 			opts.Bash = true
-			if opts.Zsh || opts.Fish {
-				errorExit("cannot specify --bash with --zsh or --fish")
-			}
 		case "--zsh":
+			clearExitingOpts()
 			opts.Zsh = true
-			if opts.Bash || opts.Fish {
-				errorExit("cannot specify --zsh with --bash or --fish")
-			}
 		case "--fish":
+			clearExitingOpts()
 			opts.Fish = true
-			if opts.Bash || opts.Zsh {
-				errorExit("cannot specify --fish with --bash or --zsh")
-			}
 		case "-h", "--help":
-			help(exitOk)
+			clearExitingOpts()
+			opts.Help = true
+		case "--version":
+			clearExitingOpts()
+			opts.Version = true
 		case "-x", "--extended":
 			opts.Extended = true
 		case "-e", "--exact":
@@ -1715,20 +1792,43 @@ func parseOptions(opts *Options, allArgs []string) {
 		case "+e", "--no-exact":
 			opts.Fuzzy = true
 		case "-q", "--query":
-			opts.Query = nextString(allArgs, &i, "query string required")
+			if opts.Query, err = nextString(allArgs, &i, "query string required"); err != nil {
+				return err
+			}
 		case "-f", "--filter":
-			filter := nextString(allArgs, &i, "query string required")
+			filter, err := nextString(allArgs, &i, "query string required")
+			if err != nil {
+				return err
+			}
 			opts.Filter = &filter
 		case "--literal":
 			opts.Normalize = false
 		case "--no-literal":
 			opts.Normalize = true
 		case "--algo":
-			opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)"))
+			str, err := nextString(allArgs, &i, "algorithm required (v1|v2)")
+			if err != nil {
+				return err
+			}
+			if opts.FuzzyAlgo, err = parseAlgo(str); err != nil {
+				return err
+			}
 		case "--scheme":
-			opts.Scheme = strings.ToLower(nextString(allArgs, &i, "scoring scheme required (default|path|history)"))
+			str, err := nextString(allArgs, &i, "scoring scheme required (default|path|history)")
+			if err != nil {
+				return err
+			}
+			opts.Scheme = strings.ToLower(str)
 		case "--expect":
-			for k, v := range parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") {
+			str, err := nextString(allArgs, &i, "key names required")
+			if err != nil {
+				return err
+			}
+			chords, err := parseKeyChords(str, "key names required")
+			if err != nil {
+				return err
+			}
+			for k, v := range chords {
 				opts.Expect[k] = v
 			}
 		case "--no-expect":
@@ -1738,26 +1838,64 @@ func parseOptions(opts *Options, allArgs []string) {
 		case "--disabled", "--phony":
 			opts.Phony = true
 		case "--tiebreak":
-			opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
+			str, err := nextString(allArgs, &i, "sort criterion required")
+			if err != nil {
+				return err
+			}
+			if opts.Criteria, err = parseTiebreak(str); err != nil {
+				return err
+			}
 		case "--bind":
-			parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"), errorExit)
+			str, err := nextString(allArgs, &i, "bind expression required")
+			if err != nil {
+				return err
+			}
+			if err := parseKeymap(opts.Keymap, str); err != nil {
+				return err
+			}
 		case "--color":
 			_, spec := optionalNextString(allArgs, &i)
 			if len(spec) == 0 {
 				opts.Theme = tui.EmptyTheme()
 			} else {
-				opts.Theme = parseTheme(opts.Theme, spec)
+				if opts.Theme, err = parseTheme(opts.Theme, spec); err != nil {
+					return err
+				}
 			}
 		case "--toggle-sort":
-			parseToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required"))
+			str, err := nextString(allArgs, &i, "key name required")
+			if err != nil {
+				return err
+			}
+			if err := parseToggleSort(opts.Keymap, str); err != nil {
+				return err
+			}
 		case "-d", "--delimiter":
-			opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required"))
+			str, err := nextString(allArgs, &i, "delimiter required")
+			if err != nil {
+				return err
+			}
+			opts.Delimiter = delimiterRegexp(str)
 		case "-n", "--nth":
-			opts.Nth = splitNth(nextString(allArgs, &i, "nth expression required"))
+			str, err := nextString(allArgs, &i, "nth expression required")
+			if err != nil {
+				return err
+			}
+			if opts.Nth, err = splitNth(str); err != nil {
+				return err
+			}
 		case "--with-nth":
-			opts.WithNth = splitNth(nextString(allArgs, &i, "nth expression required"))
+			str, err := nextString(allArgs, &i, "nth expression required")
+			if err != nil {
+				return err
+			}
+			if opts.WithNth, err = splitNth(str); err != nil {
+				return err
+			}
 		case "-s", "--sort":
-			opts.Sort = optionalNumeric(allArgs, &i, 1)
+			if opts.Sort, err = optionalNumeric(allArgs, &i, 1); err != nil {
+				return err
+			}
 		case "+s", "--no-sort":
 			opts.Sort = 0
 		case "--track":
@@ -1773,7 +1911,9 @@ func parseOptions(opts *Options, allArgs []string) {
 		case "+i":
 			opts.Case = CaseRespect
 		case "-m", "--multi":
-			opts.Multi = optionalNumeric(allArgs, &i, maxMulti)
+			if opts.Multi, err = optionalNumeric(allArgs, &i, maxMulti); err != nil {
+				return err
+			}
 		case "+m", "--no-multi":
 			opts.Multi = 0
 		case "--ansi":
@@ -1795,8 +1935,13 @@ func parseOptions(opts *Options, allArgs []string) {
 		case "--no-bold":
 			opts.Bold = false
 		case "--layout":
-			opts.Layout = parseLayout(
-				nextString(allArgs, &i, "layout required (default / reverse / reverse-list)"))
+			str, err := nextString(allArgs, &i, "layout required (default / reverse / reverse-list)")
+			if err != nil {
+				return err
+			}
+			if opts.Layout, err = parseLayout(str); err != nil {
+				return err
+			}
 		case "--reverse":
 			opts.Layout = layoutReverse
 		case "--no-reverse":
@@ -1814,16 +1959,25 @@ func parseOptions(opts *Options, allArgs []string) {
 		case "--no-hscroll":
 			opts.Hscroll = false
 		case "--hscroll-off":
-			opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required")
+			if opts.HscrollOff, err = nextInt(allArgs, &i, "hscroll offset required"); err != nil {
+				return err
+			}
 		case "--scroll-off":
-			opts.ScrollOff = nextInt(allArgs, &i, "scroll offset required")
+			if opts.ScrollOff, err = nextInt(allArgs, &i, "scroll offset required"); err != nil {
+				return err
+			}
 		case "--filepath-word":
 			opts.FileWord = true
 		case "--no-filepath-word":
 			opts.FileWord = false
 		case "--info":
-			opts.InfoStyle, opts.InfoPrefix = parseInfoStyle(
-				nextString(allArgs, &i, "info style required"))
+			str, err := nextString(allArgs, &i, "info style required")
+			if err != nil {
+				return err
+			}
+			if opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(str); err != nil {
+				return err
+			}
 		case "--no-info":
 			opts.InfoStyle = infoHidden
 		case "--inline-info":
@@ -1832,7 +1986,10 @@ func parseOptions(opts *Options, allArgs []string) {
 		case "--no-inline-info":
 			opts.InfoStyle = infoDefault
 		case "--separator":
-			separator := nextString(allArgs, &i, "separator character required")
+			separator, err := nextString(allArgs, &i, "separator character required")
+			if err != nil {
+				return err
+			}
 			opts.Separator = &separator
 		case "--no-separator":
 			nosep := ""
@@ -1848,7 +2005,9 @@ func parseOptions(opts *Options, allArgs []string) {
 			noBar := ""
 			opts.Scrollbar = &noBar
 		case "--jump-labels":
-			opts.JumpLabels = nextString(allArgs, &i, "label characters required")
+			if opts.JumpLabels, err = nextString(allArgs, &i, "label characters required"); err != nil {
+				return err
+			}
 			validateJumpLabels = true
 		case "-1", "--select-1":
 			opts.Select1 = true
@@ -1873,11 +2032,22 @@ func parseOptions(opts *Options, allArgs []string) {
 		case "--no-print-query":
 			opts.PrintQuery = false
 		case "--prompt":
-			opts.Prompt = nextString(allArgs, &i, "prompt string required")
+			opts.Prompt, err = nextString(allArgs, &i, "prompt string required")
+			if err != nil {
+				return err
+			}
 		case "--pointer":
-			opts.Pointer = firstLine(nextString(allArgs, &i, "pointer sign string required"))
+			str, err := nextString(allArgs, &i, "pointer sign string required")
+			if err != nil {
+				return err
+			}
+			opts.Pointer = firstLine(str)
 		case "--marker":
-			opts.Marker = firstLine(nextString(allArgs, &i, "selected sign string required"))
+			str, err := nextString(allArgs, &i, "selected sign string required")
+			if err != nil {
+				return err
+			}
+			opts.Marker = firstLine(str)
 		case "--sync":
 			opts.Sync = true
 		case "--no-sync", "--async":
@@ -1885,35 +2055,69 @@ func parseOptions(opts *Options, allArgs []string) {
 		case "--no-history":
 			opts.History = nil
 		case "--history":
-			setHistory(nextString(allArgs, &i, "history file path required"))
+			str, err := nextString(allArgs, &i, "history file path required")
+			if err != nil {
+				return err
+			}
+			if err := setHistory(str); err != nil {
+				return err
+			}
 		case "--history-size":
-			setHistoryMax(nextInt(allArgs, &i, "history max size required"))
+			n, err := nextInt(allArgs, &i, "history max size required")
+			if err != nil {
+				return err
+			}
+			if err := setHistoryMax(n); err != nil {
+				return err
+			}
 		case "--no-header":
 			opts.Header = []string{}
 		case "--no-header-lines":
 			opts.HeaderLines = 0
 		case "--header":
-			opts.Header = strLines(nextString(allArgs, &i, "header string required"))
+			str, err := nextString(allArgs, &i, "header string required")
+			if err != nil {
+				return err
+			}
+			opts.Header = strLines(str)
 		case "--header-lines":
-			opts.HeaderLines = atoi(
-				nextString(allArgs, &i, "number of header lines required"))
+			if opts.HeaderLines, err = nextInt(allArgs, &i, "number of header lines required"); err != nil {
+				return err
+			}
 		case "--header-first":
 			opts.HeaderFirst = true
 		case "--no-header-first":
 			opts.HeaderFirst = false
 		case "--ellipsis":
-			opts.Ellipsis = nextString(allArgs, &i, "ellipsis string required")
+			if opts.Ellipsis, err = nextString(allArgs, &i, "ellipsis string required"); err != nil {
+				return err
+			}
 		case "--preview":
-			opts.Preview.command = nextString(allArgs, &i, "preview command required")
+			if opts.Preview.command, err = nextString(allArgs, &i, "preview command required"); err != nil {
+				return err
+			}
 		case "--no-preview":
 			opts.Preview.command = ""
 		case "--preview-window":
-			parsePreviewWindow(&opts.Preview,
-				nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-BORDER_OPT][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]"))
+			str, err := nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-BORDER_OPT][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]")
+			if err != nil {
+				return err
+			}
+			if err := parsePreviewWindow(&opts.Preview, str); err != nil {
+				return err
+			}
 		case "--height":
-			opts.Height = parseHeight(nextString(allArgs, &i, "height required: [~]HEIGHT[%]"))
+			str, err := nextString(allArgs, &i, "height required: [~]HEIGHT[%]")
+			if err != nil {
+				return err
+			}
+			if opts.Height, err = parseHeight(str); err != nil {
+				return err
+			}
 		case "--min-height":
-			opts.MinHeight = nextInt(allArgs, &i, "height required: HEIGHT")
+			if opts.MinHeight, err = nextInt(allArgs, &i, "height required: HEIGHT"); err != nil {
+				return err
+			}
 		case "--no-height":
 			opts.Height = heightSpec{}
 		case "--no-margin":
@@ -1924,21 +2128,38 @@ func parseOptions(opts *Options, allArgs []string) {
 			opts.BorderShape = tui.BorderNone
 		case "--border":
 			hasArg, arg := optionalNextString(allArgs, &i)
-			opts.BorderShape = parseBorder(arg, !hasArg)
+			if opts.BorderShape, err = parseBorder(arg, !hasArg); err != nil {
+				return err
+			}
 		case "--no-border-label":
 			opts.BorderLabel.label = ""
 		case "--border-label":
-			opts.BorderLabel.label = nextString(allArgs, &i, "label required")
+			opts.BorderLabel.label, err = nextString(allArgs, &i, "label required")
+			if err != nil {
+				return err
+			}
 		case "--border-label-pos":
-			pos := nextString(allArgs, &i, "label position required (positive or negative integer or 'center')")
-			parseLabelPosition(&opts.BorderLabel, pos)
+			pos, err := nextString(allArgs, &i, "label position required (positive or negative integer or 'center')")
+			if err != nil {
+				return err
+			}
+			if err := parseLabelPosition(&opts.BorderLabel, pos); err != nil {
+				return err
+			}
 		case "--no-preview-label":
 			opts.PreviewLabel.label = ""
 		case "--preview-label":
-			opts.PreviewLabel.label = nextString(allArgs, &i, "preview label required")
+			if opts.PreviewLabel.label, err = nextString(allArgs, &i, "preview label required"); err != nil {
+				return err
+			}
 		case "--preview-label-pos":
-			pos := nextString(allArgs, &i, "preview label position required (positive or negative integer or 'center')")
-			parseLabelPosition(&opts.PreviewLabel, pos)
+			pos, err := nextString(allArgs, &i, "preview label position required (positive or negative integer or 'center')")
+			if err != nil {
+				return err
+			}
+			if err := parseLabelPosition(&opts.PreviewLabel, pos); err != nil {
+				return err
+			}
 		case "--no-unicode":
 			opts.Unicode = false
 		case "--unicode":
@@ -1948,17 +2169,29 @@ func parseOptions(opts *Options, allArgs []string) {
 		case "--no-ambidouble":
 			opts.Ambidouble = false
 		case "--margin":
-			opts.Margin = parseMargin(
-				"margin",
-				nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
+			str, err := nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)")
+			if err != nil {
+				return err
+			}
+			if opts.Margin, err = parseMargin("margin", str); err != nil {
+				return err
+			}
 		case "--padding":
-			opts.Padding = parseMargin(
-				"padding",
-				nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
+			str, err := nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)")
+			if err != nil {
+				return err
+			}
+			if opts.Padding, err = parseMargin("padding", str); err != nil {
+				return err
+			}
 		case "--tabstop":
-			opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
+			if opts.Tabstop, err = nextInt(allArgs, &i, "tab stop required"); err != nil {
+				return err
+			}
 		case "--with-shell":
-			opts.WithShell = nextString(allArgs, &i, "shell command and flags required")
+			if opts.WithShell, err = nextString(allArgs, &i, "shell command and flags required"); err != nil {
+				return err
+			}
 		case "--listen", "--listen-unsafe":
 			given, str := optionalNextString(allArgs, &i)
 			addr := defaultListenAddr
@@ -1966,7 +2199,7 @@ func parseOptions(opts *Options, allArgs []string) {
 				var err error
 				addr, err = parseListenAddress(str)
 				if err != nil {
-					errorExit(err.Error())
+					return err
 				}
 			}
 			opts.ListenAddr = &addr
@@ -1979,26 +2212,46 @@ func parseOptions(opts *Options, allArgs []string) {
 		case "--no-clear":
 			opts.ClearOnExit = false
 		case "--walker":
-			opts.WalkerOpts = parseWalkerOpts(nextString(allArgs, &i, "walker options required [file][,dir][,follow][,hidden]"))
+			str, err := nextString(allArgs, &i, "walker options required [file][,dir][,follow][,hidden]")
+			if err != nil {
+				return err
+			}
+			if opts.WalkerOpts, err = parseWalkerOpts(str); err != nil {
+				return err
+			}
 		case "--walker-root":
-			opts.WalkerRoot = nextString(allArgs, &i, "directory required")
+			if opts.WalkerRoot, err = nextString(allArgs, &i, "directory required"); err != nil {
+				return err
+			}
 		case "--walker-skip":
-			opts.WalkerSkip = filterNonEmpty(strings.Split(nextString(allArgs, &i, "directory names to ignore required"), ","))
-		case "--version":
-			opts.Version = true
+			str, err := nextString(allArgs, &i, "directory names to ignore required")
+			if err != nil {
+				return err
+			}
+			opts.WalkerSkip = filterNonEmpty(strings.Split(str, ","))
 		case "--profile-cpu":
-			opts.CPUProfile = nextString(allArgs, &i, "file path required: cpu")
+			if opts.CPUProfile, err = nextString(allArgs, &i, "file path required: cpu"); err != nil {
+				return err
+			}
 		case "--profile-mem":
-			opts.MEMProfile = nextString(allArgs, &i, "file path required: mem")
+			if opts.MEMProfile, err = nextString(allArgs, &i, "file path required: mem"); err != nil {
+				return err
+			}
 		case "--profile-block":
-			opts.BlockProfile = nextString(allArgs, &i, "file path required: block")
+			if opts.BlockProfile, err = nextString(allArgs, &i, "file path required: block"); err != nil {
+				return err
+			}
 		case "--profile-mutex":
-			opts.MutexProfile = nextString(allArgs, &i, "file path required: mutex")
+			if opts.MutexProfile, err = nextString(allArgs, &i, "file path required: mutex"); err != nil {
+				return err
+			}
 		case "--":
 			// Ignored
 		default:
 			if match, value := optString(arg, "--algo="); match {
-				opts.FuzzyAlgo = parseAlgo(value)
+				if opts.FuzzyAlgo, err = parseAlgo(value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--scheme="); match {
 				opts.Scheme = strings.ToLower(value)
 			} else if match, value := optString(arg, "-q", "--query="); match {
@@ -2008,15 +2261,21 @@ func parseOptions(opts *Options, allArgs []string) {
 			} else if match, value := optString(arg, "-d", "--delimiter="); match {
 				opts.Delimiter = delimiterRegexp(value)
 			} else if match, value := optString(arg, "--border="); match {
-				opts.BorderShape = parseBorder(value, false)
+				if opts.BorderShape, err = parseBorder(value, false); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--border-label="); match {
 				opts.BorderLabel.label = value
 			} else if match, value := optString(arg, "--border-label-pos="); match {
-				parseLabelPosition(&opts.BorderLabel, value)
+				if err := parseLabelPosition(&opts.BorderLabel, value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--preview-label="); match {
 				opts.PreviewLabel.label = value
 			} else if match, value := optString(arg, "--preview-label-pos="); match {
-				parseLabelPosition(&opts.PreviewLabel, value)
+				if err := parseLabelPosition(&opts.PreviewLabel, value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--prompt="); match {
 				opts.Prompt = value
 			} else if match, value := optString(arg, "--pointer="); match {
@@ -2024,119 +2283,170 @@ func parseOptions(opts *Options, allArgs []string) {
 			} else if match, value := optString(arg, "--marker="); match {
 				opts.Marker = firstLine(value)
 			} else if match, value := optString(arg, "-n", "--nth="); match {
-				opts.Nth = splitNth(value)
+				if opts.Nth, err = splitNth(value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--with-nth="); match {
-				opts.WithNth = splitNth(value)
+				if opts.WithNth, err = splitNth(value); err != nil {
+					return err
+				}
 			} else if match, _ := optString(arg, "-s", "--sort="); match {
 				opts.Sort = 1 // Don't care
 			} else if match, value := optString(arg, "-m", "--multi="); match {
-				opts.Multi = atoi(value)
+				if opts.Multi, err = atoi(value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--height="); match {
-				opts.Height = parseHeight(value)
+				if opts.Height, err = parseHeight(value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--min-height="); match {
-				opts.MinHeight = atoi(value)
+				if opts.MinHeight, err = atoi(value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--layout="); match {
-				opts.Layout = parseLayout(value)
+				if opts.Layout, err = parseLayout(value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--info="); match {
-				opts.InfoStyle, opts.InfoPrefix = parseInfoStyle(value)
+				if opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--separator="); match {
 				opts.Separator = &value
 			} else if match, value := optString(arg, "--scrollbar="); match {
 				opts.Scrollbar = &value
 			} else if match, value := optString(arg, "--toggle-sort="); match {
-				parseToggleSort(opts.Keymap, value)
+				if err := parseToggleSort(opts.Keymap, value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--expect="); match {
-				for k, v := range parseKeyChords(value, "key names required") {
+				chords, err := parseKeyChords(value, "key names required")
+				if err != nil {
+					return err
+				}
+				for k, v := range chords {
 					opts.Expect[k] = v
 				}
 			} else if match, value := optString(arg, "--tiebreak="); match {
-				opts.Criteria = parseTiebreak(value)
+				if opts.Criteria, err = parseTiebreak(value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--color="); match {
-				opts.Theme = parseTheme(opts.Theme, value)
+				if opts.Theme, err = parseTheme(opts.Theme, value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--bind="); match {
-				parseKeymap(opts.Keymap, value, errorExit)
+				if err := parseKeymap(opts.Keymap, value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--history="); match {
-				setHistory(value)
+				if err := setHistory(value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--history-size="); match {
-				setHistoryMax(atoi(value))
+				n, err := atoi(value)
+				if err != nil {
+					return err
+				}
+				if err := setHistoryMax(n); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--header="); match {
 				opts.Header = strLines(value)
 			} else if match, value := optString(arg, "--header-lines="); match {
-				opts.HeaderLines = atoi(value)
+				if opts.HeaderLines, err = atoi(value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--ellipsis="); match {
 				opts.Ellipsis = value
 			} else if match, value := optString(arg, "--preview="); match {
 				opts.Preview.command = value
 			} else if match, value := optString(arg, "--preview-window="); match {
-				parsePreviewWindow(&opts.Preview, value)
+				if err := parsePreviewWindow(&opts.Preview, value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--margin="); match {
-				opts.Margin = parseMargin("margin", value)
+				if opts.Margin, err = parseMargin("margin", value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--padding="); match {
-				opts.Padding = parseMargin("padding", value)
+				if opts.Padding, err = parseMargin("padding", value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--tabstop="); match {
-				opts.Tabstop = atoi(value)
+				if opts.Tabstop, err = atoi(value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--with-shell="); match {
 				opts.WithShell = value
 			} else if match, value := optString(arg, "--listen="); match {
 				addr, err := parseListenAddress(value)
 				if err != nil {
-					errorExit(err.Error())
+					return err
 				}
 				opts.ListenAddr = &addr
 				opts.Unsafe = false
 			} else if match, value := optString(arg, "--listen-unsafe="); match {
 				addr, err := parseListenAddress(value)
 				if err != nil {
-					errorExit(err.Error())
+					return err
 				}
 				opts.ListenAddr = &addr
 				opts.Unsafe = true
 			} else if match, value := optString(arg, "--walker="); match {
-				opts.WalkerOpts = parseWalkerOpts(value)
+				if opts.WalkerOpts, err = parseWalkerOpts(value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--walker-root="); match {
 				opts.WalkerRoot = value
 			} else if match, value := optString(arg, "--walker-skip="); match {
 				opts.WalkerSkip = filterNonEmpty(strings.Split(value, ","))
 			} else if match, value := optString(arg, "--hscroll-off="); match {
-				opts.HscrollOff = atoi(value)
+				if opts.HscrollOff, err = atoi(value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--scroll-off="); match {
-				opts.ScrollOff = atoi(value)
+				if opts.ScrollOff, err = atoi(value); err != nil {
+					return err
+				}
 			} else if match, value := optString(arg, "--jump-labels="); match {
 				opts.JumpLabels = value
 				validateJumpLabels = true
 			} else {
-				errorExit("unknown option: " + arg)
+				return errors.New("unknown option: " + arg)
 			}
 		}
 	}
 
 	if opts.HeaderLines < 0 {
-		errorExit("header lines must be a non-negative integer")
+		return errors.New("header lines must be a non-negative integer")
 	}
 
 	if opts.HscrollOff < 0 {
-		errorExit("hscroll offset must be a non-negative integer")
+		return errors.New("hscroll offset must be a non-negative integer")
 	}
 
 	if opts.ScrollOff < 0 {
-		errorExit("scroll offset must be a non-negative integer")
+		return errors.New("scroll offset must be a non-negative integer")
 	}
 
 	if opts.Tabstop < 1 {
-		errorExit("tab stop must be a positive integer")
+		return errors.New("tab stop must be a positive integer")
 	}
 
 	if len(opts.JumpLabels) == 0 {
-		errorExit("empty jump labels")
+		return errors.New("empty jump labels")
 	}
 
 	if validateJumpLabels {
 		for _, r := range opts.JumpLabels {
 			if r < 32 || r > 126 {
-				errorExit("non-ascii jump labels are not allowed")
+				return errors.New("non-ascii jump labels are not allowed")
 			}
 		}
 	}
+	return err
 }
 
 func validateSign(sign string, signOptName string) error {
@@ -2149,31 +2459,33 @@ func validateSign(sign string, signOptName string) error {
 	return nil
 }
 
-func postProcessOptions(opts *Options) {
+// This function can have side-effects and alter some global states.
+// So we run it on fzf.Run and not on ParseOptions.
+func postProcessOptions(opts *Options) error {
 	if opts.Ambidouble {
 		uniseg.EastAsianAmbiguousWidth = 2
 	}
 
 	if err := validateSign(opts.Pointer, "pointer"); err != nil {
-		errorExit(err.Error())
+		return err
 	}
 
 	if err := validateSign(opts.Marker, "marker"); err != nil {
-		errorExit(err.Error())
+		return err
 	}
 
-	if !opts.Version && !tui.IsLightRendererSupported() && opts.Height.size > 0 {
-		errorExit("--height option is currently not supported on this platform")
+	if !tui.IsLightRendererSupported() && opts.Height.size > 0 {
+		return errors.New("--height option is currently not supported on this platform")
 	}
 
 	if opts.Scrollbar != nil {
 		runes := []rune(*opts.Scrollbar)
 		if len(runes) > 2 {
-			errorExit("--scrollbar should be given one or two characters")
+			return errors.New("--scrollbar should be given one or two characters")
 		}
 		for _, r := range runes {
 			if uniseg.StringWidth(string(r)) != 1 {
-				errorExit("scrollbar display width should be 1")
+				return errors.New("scrollbar display width should be 1")
 			}
 		}
 	}
@@ -2227,12 +2539,12 @@ func postProcessOptions(opts *Options) {
 	if opts.Height.auto {
 		for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
 			if s.percent {
-				errorExit("adaptive height is not compatible with top/bottom percent margin")
+				return errors.New("adaptive height is not compatible with top/bottom percent margin")
 			}
 		}
 		for _, s := range []sizeSpec{opts.Padding[0], opts.Padding[2]} {
 			if s.percent {
-				errorExit("adaptive height is not compatible with top/bottom percent padding")
+				return errors.New("adaptive height is not compatible with top/bottom percent padding")
 			}
 		}
 	}
@@ -2243,7 +2555,7 @@ func postProcessOptions(opts *Options) {
 		for _, r := range opts.Nth {
 			if r.begin == rangeEllipsis && r.end == rangeEllipsis {
 				opts.Nth = make([]Range, 0)
-				return
+				break
 			}
 		}
 	}
@@ -2265,65 +2577,52 @@ func postProcessOptions(opts *Options) {
 		theme.Spinner = boldify(theme.Spinner)
 	}
 
-	processScheme(opts)
-}
-
-func expectsArbitraryString(opt string) bool {
-	switch opt {
-	case "-q", "--query", "-f", "--filter", "--header", "--prompt":
-		return true
-	}
-	return false
+	return processScheme(opts)
 }
 
 // ParseOptions parses command-line options
-func ParseOptions() *Options {
+func ParseOptions(useDefaults bool, args []string) (*Options, error) {
 	opts := defaultOptions()
 
-	for idx, arg := range os.Args[1:] {
-		if arg == "--version" && (idx == 0 || idx > 0 && !expectsArbitraryString(os.Args[idx])) {
-			opts.Version = true
-			return opts
-		}
-	}
+	if useDefaults {
+		// 1. Options from $FZF_DEFAULT_OPTS_FILE
+		if path := os.Getenv("FZF_DEFAULT_OPTS_FILE"); path != "" {
+			bytes, err := os.ReadFile(path)
+			if err != nil {
+				return nil, errors.New("$FZF_DEFAULT_OPTS_FILE: " + err.Error())
+			}
 
-	// 1. Options from $FZF_DEFAULT_OPTS_FILE
-	if path := os.Getenv("FZF_DEFAULT_OPTS_FILE"); path != "" {
-		bytes, err := os.ReadFile(path)
-		if err != nil {
-			errorContext = "$FZF_DEFAULT_OPTS_FILE: "
-			errorExit(err.Error())
+			words, parseErr := shellwords.Parse(string(bytes))
+			if parseErr != nil {
+				return nil, errors.New(path + ": " + parseErr.Error())
+			}
+			if len(words) > 0 {
+				if err := parseOptions(opts, words); err != nil {
+					return nil, errors.New(path + ": " + err.Error())
+				}
+			}
 		}
 
-		words, parseErr := shellwords.Parse(string(bytes))
+		// 2. Options from $FZF_DEFAULT_OPTS string
+		words, parseErr := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))
 		if parseErr != nil {
-			errorContext = path + ": "
-			errorExit(parseErr.Error())
+			return nil, errors.New("$FZF_DEFAULT_OPTS: " + parseErr.Error())
 		}
 		if len(words) > 0 {
-			parseOptions(opts, words)
+			if err := parseOptions(opts, words); err != nil {
+				return nil, errors.New("$FZF_DEFAULT_OPTS: " + err.Error())
+			}
 		}
 	}
 
-	// 2. Options from $FZF_DEFAULT_OPTS string
-	words, parseErr := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))
-	errorContext = "$FZF_DEFAULT_OPTS: "
-	if parseErr != nil {
-		errorExit(parseErr.Error())
-	}
-	if len(words) > 0 {
-		parseOptions(opts, words)
-	}
-
 	// 3. Options from command-line arguments
-	errorContext = ""
-	parseOptions(opts, os.Args[1:])
+	if err := 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)
-
-	return opts
+	return opts, nil
 }
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/pattern.go b/src/pattern.go
index bf92ca19..cbe73dc4 100644
--- a/src/pattern.go
+++ b/src/pattern.go
@@ -60,32 +60,17 @@ type Pattern struct {
 	delimiter     Delimiter
 	nth           []Range
 	procFun       map[termType]algo.Algo
+	cache         *ChunkCache
 }
 
-var (
-	_patternCache map[string]*Pattern
-	_splitRegex   *regexp.Regexp
-	_cache        ChunkCache
-)
+var _splitRegex *regexp.Regexp
 
 func init() {
 	_splitRegex = regexp.MustCompile(" +")
-	clearPatternCache()
-	clearChunkCache()
-}
-
-func clearPatternCache() {
-	// We can uniquely identify the pattern for a given string since
-	// search mode and caseMode do not change while the program is running
-	_patternCache = make(map[string]*Pattern)
-}
-
-func clearChunkCache() {
-	_cache = NewChunkCache()
 }
 
 // BuildPattern builds Pattern object from the given arguments
-func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
+func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
 	withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
 
 	var asString string
@@ -98,7 +83,9 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
 		asString = string(runes)
 	}
 
-	cached, found := _patternCache[asString]
+	// We can uniquely identify the pattern for a given string since
+	// search mode and caseMode do not change while the program is running
+	cached, found := patternCache[asString]
 	if found {
 		return cached
 	}
@@ -153,6 +140,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
 		cacheable:     cacheable,
 		nth:           nth,
 		delimiter:     delimiter,
+		cache:         cache,
 		procFun:       make(map[termType]algo.Algo)}
 
 	ptr.cacheKey = ptr.buildCacheKey()
@@ -162,7 +150,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
 	ptr.procFun[termPrefix] = algo.PrefixMatch
 	ptr.procFun[termSuffix] = algo.SuffixMatch
 
-	_patternCache[asString] = ptr
+	patternCache[asString] = ptr
 	return ptr
 }
 
@@ -282,18 +270,18 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
 	// ChunkCache: Exact match
 	cacheKey := p.CacheKey()
 	if p.cacheable {
-		if cached := _cache.Lookup(chunk, cacheKey); cached != nil {
+		if cached := p.cache.Lookup(chunk, cacheKey); cached != nil {
 			return cached
 		}
 	}
 
 	// Prefix/suffix cache
-	space := _cache.Search(chunk, cacheKey)
+	space := p.cache.Search(chunk, cacheKey)
 
 	matches := p.matchChunk(chunk, space, slab)
 
 	if p.cacheable {
-		_cache.Add(chunk, cacheKey, matches)
+		p.cache.Add(chunk, cacheKey, matches)
 	}
 	return matches
 }
diff --git a/src/protector/protector.go b/src/protector/protector.go
index fe84b388..9758e1e7 100644
--- a/src/protector/protector.go
+++ b/src/protector/protector.go
@@ -3,6 +3,4 @@
 package protector
 
 // Protect calls OS specific protections like pledge on OpenBSD
-func Protect() {
-	return
-}
+func Protect() {}
diff --git a/src/reader.go b/src/reader.go
index 8fa864e7..6092087e 100644
--- a/src/reader.go
+++ b/src/reader.go
@@ -93,11 +93,26 @@ func (r *Reader) restart(command string, environ []string) {
 	r.fin(success)
 }
 
+func (r *Reader) readChannel(inputChan chan string) bool {
+	for {
+		item, more := <-inputChan
+		if !more {
+			break
+		}
+		if r.pusher([]byte(item)) {
+			atomic.StoreInt32(&r.event, int32(EvtReadNew))
+		}
+	}
+	return true
+}
+
 // ReadSource reads data from the default command or from standard input
-func (r *Reader) ReadSource(root string, opts walkerOpts, ignores []string) {
+func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string) {
 	r.startEventPoller()
 	var success bool
-	if util.IsTty() {
+	if inputChan != nil {
+		success = r.readChannel(inputChan)
+	} else if util.IsTty() {
 		cmd := os.Getenv("FZF_DEFAULT_COMMAND")
 		if len(cmd) == 0 {
 			success = r.readFiles(root, opts, ignores)
diff --git a/src/server.go b/src/server.go
index aa80eb4f..0f325d8c 100644
--- a/src/server.go
+++ b/src/server.go
@@ -73,28 +73,28 @@ func parseListenAddress(address string) (listenAddress, error) {
 	return listenAddress{parts[0], port}, nil
 }
 
-func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (int, error) {
+func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (net.Listener, int, error) {
 	host := address.host
 	port := address.port
 	apiKey := os.Getenv("FZF_API_KEY")
 	if !address.IsLocal() && len(apiKey) == 0 {
-		return port, errors.New("FZF_API_KEY is required to allow remote access")
+		return nil, port, errors.New("FZF_API_KEY is required to allow remote access")
 	}
 	addrStr := fmt.Sprintf("%s:%d", host, port)
 	listener, err := net.Listen("tcp", addrStr)
 	if err != nil {
-		return port, fmt.Errorf("failed to listen on %s", addrStr)
+		return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
 	}
 	if port == 0 {
 		addr := listener.Addr().String()
 		parts := strings.Split(addr, ":")
 		if len(parts) < 2 {
-			return port, fmt.Errorf("cannot extract port: %s", addr)
+			return nil, port, fmt.Errorf("cannot extract port: %s", addr)
 		}
 		var err error
 		port, err = strconv.Atoi(parts[len(parts)-1])
 		if err != nil {
-			return port, err
+			return nil, port, err
 		}
 	}
 
@@ -109,18 +109,16 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon
 			conn, err := listener.Accept()
 			if err != nil {
 				if errors.Is(err, net.ErrClosed) {
-					break
-				} else {
-					continue
+					return
 				}
+				continue
 			}
 			conn.Write([]byte(server.handleHttpRequest(conn)))
 			conn.Close()
 		}
-		listener.Close()
 	}()
 
-	return port, nil
+	return listener, port, nil
 }
 
 // Here we are writing a simplistic HTTP server without using net/http
@@ -217,12 +215,9 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
 	}
 	body = body[:contentLength]
 
-	errorMessage := ""
-	actions := parseSingleActionList(strings.Trim(string(body), "\r\n"), func(message string) {
-		errorMessage = message
-	})
-	if len(errorMessage) > 0 {
-		return bad(errorMessage)
+	actions, err := parseSingleActionList(strings.Trim(string(body), "\r\n"))
+	if err != nil {
+		return bad(err.Error())
 	}
 	if len(actions) == 0 {
 		return bad("no action specified")
diff --git a/src/terminal.go b/src/terminal.go
index 951b8c3b..18915035 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -2,10 +2,12 @@ package fzf
 
 import (
 	"bufio"
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
 	"math"
+	"net"
 	"os"
 	"os/signal"
 	"regexp"
@@ -49,8 +51,8 @@ var whiteSuffix *regexp.Regexp
 var offsetComponentRegex *regexp.Regexp
 var offsetTrimCharsRegex *regexp.Regexp
 var activeTempFiles []string
+var activeTempFilesMutex sync.Mutex
 var passThroughRegex *regexp.Regexp
-var actionTypeRegex *regexp.Regexp
 
 const clearCode string = "\x1b[2J"
 
@@ -63,6 +65,7 @@ func init() {
 	offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
 	offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
 	activeTempFiles = []string{}
+	activeTempFilesMutex = sync.Mutex{}
 
 	// Parts of the preview output that should be passed through to the terminal
 	// * https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it
@@ -241,6 +244,7 @@ type Terminal struct {
 	unicode            bool
 	listenAddr         *listenAddress
 	listenPort         *int
+	listener           net.Listener
 	listenUnsafe       bool
 	borderShape        tui.BorderShape
 	cleanExit          bool
@@ -259,7 +263,7 @@ type Terminal struct {
 	hasResizeActions   bool
 	triggerLoad        bool
 	reading            bool
-	running            bool
+	running            *util.AtomicBool
 	failed             *string
 	jumping            jumpMode
 	jumpLabels         string
@@ -278,12 +282,12 @@ type Terminal struct {
 	previewBox         *util.EventBox
 	eventBox           *util.EventBox
 	mutex              sync.Mutex
-	initFunc           func()
+	initFunc           func() error
 	prevLines          []itemLine
 	suppress           bool
 	sigstop            bool
 	startChan          chan fitpad
-	killChan           chan int
+	killChan           chan bool
 	serverInputChan    chan []*action
 	serverOutputChan   chan string
 	eventChan          chan tui.Event
@@ -340,6 +344,7 @@ const (
 	reqPreviewRefresh
 	reqPreviewDelayed
 	reqQuit
+	reqFatal
 )
 
 type action struct {
@@ -380,6 +385,7 @@ const (
 	actDeleteChar
 	actDeleteCharEof
 	actEndOfLine
+	actFatal
 	actForwardChar
 	actForwardWord
 	actKillLine
@@ -511,6 +517,7 @@ type previewRequest struct {
 	pwindowSize  tui.TermSize
 	scrollOffset int
 	list         []*Item
+	env          []string
 }
 
 type previewResult struct {
@@ -537,6 +544,7 @@ func defaultKeymap() map[tui.Event][]*action {
 		keymap[e] = toActions(a)
 	}
 
+	add(tui.Fatal, actFatal)
 	add(tui.Invalid, actInvalid)
 	add(tui.CtrlA, actBeginningOfLine)
 	add(tui.CtrlB, actBackwardChar)
@@ -642,7 +650,7 @@ func evaluateHeight(opts *Options, termHeight int) int {
 }
 
 // NewTerminal returns new Terminal object
-func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor) *Terminal {
+func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor) (*Terminal, error) {
 	input := trimQuery(opts.Query)
 	var delay time.Duration
 	if opts.Tac {
@@ -660,11 +668,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
 	}
 	var renderer tui.Renderer
 	fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100)
+	var err error
 	if fullscreen {
 		if tui.HasFullscreenRenderer() {
 			renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
 		} else {
-			renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
+			renderer, err = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
 				true, func(h int) int { return h })
 		}
 	} else {
@@ -680,7 +689,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
 			effectiveMinHeight += borderLines(opts.BorderShape)
 			return util.Min(termHeight, util.Max(evaluateHeight(opts, termHeight), effectiveMinHeight))
 		}
-		renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
+		renderer, err = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
+	}
+	if err != nil {
+		return nil, err
 	}
 	wordRubout := "[^\\pL\\pN][\\pL\\pN]"
 	wordNext := "[\\pL\\pN][^\\pL\\pN]|(.$)"
@@ -693,6 +705,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
 	for key, action := range opts.Keymap {
 		keymapCopy[key] = action
 	}
+
 	t := Terminal{
 		initDelay:          delay,
 		infoStyle:          opts.InfoStyle,
@@ -754,7 +767,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
 		hasLoadActions:     false,
 		triggerLoad:        false,
 		reading:            true,
-		running:            true,
+		running:            util.NewAtomicBool(true),
 		failed:             nil,
 		jumping:            jumpDisabled,
 		jumpLabels:         opts.JumpLabels,
@@ -775,12 +788,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
 		slab:               util.MakeSlab(slab16Size, slab32Size),
 		theme:              opts.Theme,
 		startChan:          make(chan fitpad, 1),
-		killChan:           make(chan int),
+		killChan:           make(chan bool),
 		serverInputChan:    make(chan []*action, 100),
 		serverOutputChan:   make(chan string),
 		eventChan:          make(chan tui.Event, 6), // (load + result + zero|one) | (focus) | (resize) | (GetChar)
 		tui:                renderer,
-		initFunc:           func() { renderer.Init() },
+		initFunc:           func() error { return renderer.Init() },
 		executing:          util.NewAtomicBool(false),
 		lastAction:         actStart,
 		lastFocus:          minItem.Index()}
@@ -832,14 +845,15 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
 	_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
 
 	if t.listenAddr != nil {
-		port, err := startHttpServer(*t.listenAddr, t.serverInputChan, t.serverOutputChan)
+		listener, port, err := startHttpServer(*t.listenAddr, t.serverInputChan, t.serverOutputChan)
 		if err != nil {
-			errorExit(err.Error())
+			return nil, err
 		}
+		t.listener = listener
 		t.listenPort = &port
 	}
 
-	return &t
+	return &t, nil
 }
 
 func (t *Terminal) environ() []string {
@@ -2103,12 +2117,11 @@ func (t *Terminal) renderPreviewArea(unchanged bool) {
 	}
 
 	height := t.pwindow.Height()
-	header := []string{}
 	body := t.previewer.lines
 	headerLines := t.previewOpts.headerLines
 	// Do not enable preview header lines if it's value is too large
 	if headerLines > 0 && headerLines < util.Min(len(body), height) {
-		header = t.previewer.lines[0:headerLines]
+		header := t.previewer.lines[0:headerLines]
 		body = t.previewer.lines[headerLines:]
 		// Always redraw header
 		t.renderPreviewText(height, header, 0, false)
@@ -2232,9 +2245,8 @@ Loop:
 						t.pwindow.Move(height-1, maxWidth-1)
 						t.previewed.filled = true
 						break Loop
-					} else {
-						t.pwindow.MoveAndClear(y+requiredLines, 0)
 					}
+					t.pwindow.MoveAndClear(y+requiredLines, 0)
 				}
 			}
 
@@ -2485,8 +2497,6 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
 		case 'q':
 			flags.forceUpdate = true
 			// query flag is not skipped
-		default:
-			break
 		}
 	}
 
@@ -2512,21 +2522,27 @@ func hasPreviewFlags(template string) (slot bool, plus bool, forceUpdate bool) {
 func writeTemporaryFile(data []string, printSep string) string {
 	f, err := os.CreateTemp("", "fzf-preview-*")
 	if err != nil {
-		errorExit("Unable to create temporary file")
+		// Unable to create temporary file
+		// FIXME: Should we terminate the program?
+		return ""
 	}
 	defer f.Close()
 
 	f.WriteString(strings.Join(data, printSep))
 	f.WriteString(printSep)
+	activeTempFilesMutex.Lock()
 	activeTempFiles = append(activeTempFiles, f.Name())
+	activeTempFilesMutex.Unlock()
 	return f.Name()
 }
 
 func cleanTemporaryFiles() {
+	activeTempFilesMutex.Lock()
 	for _, filename := range activeTempFiles {
 		os.Remove(filename)
 	}
 	activeTempFiles = []string{}
+	activeTempFilesMutex.Unlock()
 }
 
 type replacePlaceholderParams struct {
@@ -2836,18 +2852,18 @@ func (t *Terminal) toggleItem(item *Item) bool {
 	return true
 }
 
-func (t *Terminal) killPreview(code int) {
+func (t *Terminal) killPreview() {
 	select {
-	case t.killChan <- code:
+	case t.killChan <- true:
 	default:
-		if code != exitCancel {
-			t.eventBox.Set(EvtQuit, code)
-		}
 	}
 }
 
 func (t *Terminal) cancelPreview() {
-	t.killPreview(exitCancel)
+	select {
+	case t.killChan <- false:
+	default:
+	}
 }
 
 func (t *Terminal) pwindowSize() tui.TermSize {
@@ -2871,7 +2887,7 @@ func (t *Terminal) currentIndex() int32 {
 }
 
 // Loop is called to start Terminal I/O
-func (t *Terminal) Loop() {
+func (t *Terminal) Loop() error {
 	// prof := profile.Start(profile.ProfilePath("/tmp/"))
 	fitpad := <-t.startChan
 	fit := fitpad.fit
@@ -2895,14 +2911,23 @@ func (t *Terminal) Loop() {
 			return util.Min(termHeight, contentHeight+pad)
 		})
 	}
+
+	// Context
+	ctx, cancel := context.WithCancel(context.Background())
+
 	{ // Late initialization
 		intChan := make(chan os.Signal, 1)
 		signal.Notify(intChan, os.Interrupt, syscall.SIGTERM)
 		go func() {
-			for s := range intChan {
-				// Don't quit by SIGINT while executing because it should be for the executing command and not for fzf itself
-				if !(s == os.Interrupt && t.executing.Get()) {
-					t.reqBox.Set(reqQuit, nil)
+			for {
+				select {
+				case <-ctx.Done():
+					return
+				case s := <-intChan:
+					// Don't quit by SIGINT while executing because it should be for the executing command and not for fzf itself
+					if !(s == os.Interrupt && t.executing.Get()) {
+						t.reqBox.Set(reqQuit, nil)
+					}
 				}
 			}
 		}()
@@ -2911,8 +2936,12 @@ func (t *Terminal) Loop() {
 		notifyOnCont(contChan)
 		go func() {
 			for {
-				<-contChan
-				t.reqBox.Set(reqReinit, nil)
+				select {
+				case <-ctx.Done():
+					return
+				case <-contChan:
+					t.reqBox.Set(reqReinit, nil)
+				}
 			}
 		}()
 
@@ -2921,14 +2950,23 @@ func (t *Terminal) Loop() {
 			notifyOnResize(resizeChan) // Non-portable
 			go func() {
 				for {
-					<-resizeChan
-					t.reqBox.Set(reqResize, nil)
+					select {
+					case <-ctx.Done():
+						return
+					case <-resizeChan:
+						t.reqBox.Set(reqResize, nil)
+					}
 				}
 			}()
 		}
 
 		t.mutex.Lock()
-		t.initFunc()
+		if err := t.initFunc(); err != nil {
+			t.mutex.Unlock()
+			cancel()
+			t.eventBox.Set(EvtQuit, quitSignal{ExitError, err})
+			return err
+		}
 		t.termSize = t.tui.Size()
 		t.resizeWindows(false)
 		t.window.Erase()
@@ -2945,7 +2983,7 @@ func (t *Terminal) Loop() {
 
 		// Keep the spinner spinning
 		go func() {
-			for {
+			for t.running.Get() {
 				t.mutex.Lock()
 				reading := t.reading
 				t.mutex.Unlock()
@@ -2960,15 +2998,20 @@ func (t *Terminal) Loop() {
 	if t.hasPreviewer() {
 		go func() {
 			var version int64
+			stop := false
 			for {
 				var items []*Item
 				var commandTemplate string
 				var pwindow tui.Window
 				var pwindowSize tui.TermSize
+				var env []string
 				initialOffset := 0
 				t.previewBox.Wait(func(events *util.Events) {
 					for req, value := range *events {
 						switch req {
+						case reqQuit:
+							stop = true
+							return
 						case reqPreviewEnqueue:
 							request := value.(previewRequest)
 							commandTemplate = request.template
@@ -2976,17 +3019,20 @@ func (t *Terminal) Loop() {
 							items = request.list
 							pwindow = request.pwindow
 							pwindowSize = request.pwindowSize
+							env = request.env
 						}
 					}
 					events.Clear()
 				})
+				if stop {
+					break
+				}
 				version++
 				// We don't display preview window if no match
 				if items[0] != nil {
 					_, query := t.Input()
 					command := t.replacePlaceholder(commandTemplate, false, string(query), items)
 					cmd := t.executor.ExecCommand(command, true)
-					env := t.environ()
 					if pwindowSize.Lines > 0 {
 						lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines)
 						columns := fmt.Sprintf("COLUMNS=%d", pwindowSize.Columns)
@@ -3071,12 +3117,13 @@ func (t *Terminal) Loop() {
 						Loop:
 							for {
 								select {
+								case <-ctx.Done():
+									break Loop
 								case <-timer.C:
 									t.reqBox.Set(reqPreviewDelayed, version)
-								case code := <-t.killChan:
-									if code != exitCancel {
+								case immediately := <-t.killChan:
+									if immediately {
 										util.KillCommand(cmd)
-										t.eventBox.Set(EvtQuit, code)
 									} else {
 										// We can immediately kill a long-running preview program
 										// once we started rendering its partial output
@@ -3123,19 +3170,25 @@ func (t *Terminal) Loop() {
 		if len(command) > 0 && t.canPreview() {
 			_, list := t.buildPlusList(command, false)
 			t.cancelPreview()
-			t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list})
+			t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list, t.environ()})
 		}
 	}
 
 	go func() {
-		var focusedIndex int32 = minItem.Index()
+		var focusedIndex = minItem.Index()
 		var version int64 = -1
 		running := true
-		code := exitError
+		code := ExitError
 		exit := func(getCode func() int) {
+			if t.hasPreviewer() {
+				t.previewBox.Set(reqQuit, nil)
+			}
+			if t.listener != nil {
+				t.listener.Close()
+			}
 			t.tui.Close()
 			code = getCode()
-			if code <= exitNoMatch && t.history != nil {
+			if code <= ExitNoMatch && t.history != nil {
 				t.history.append(string(t.input))
 			}
 			running = false
@@ -3203,9 +3256,9 @@ func (t *Terminal) Loop() {
 					case reqClose:
 						exit(func() int {
 							if t.output() {
-								return exitOk
+								return ExitOk
 							}
-							return exitNoMatch
+							return ExitNoMatch
 						})
 						return
 					case reqPreviewDisplay:
@@ -3233,11 +3286,14 @@ func (t *Terminal) Loop() {
 					case reqPrintQuery:
 						exit(func() int {
 							t.printer(string(t.input))
-							return exitOk
+							return ExitOk
 						})
 						return
 					case reqQuit:
-						exit(func() int { return exitInterrupt })
+						exit(func() int { return ExitInterrupt })
+						return
+					case reqFatal:
+						exit(func() int { return ExitError })
 						return
 					}
 				}
@@ -3245,8 +3301,11 @@ func (t *Terminal) Loop() {
 				t.mutex.Unlock()
 			})
 		}
-		// prof.Stop()
-		t.killPreview(code)
+
+		t.eventBox.Set(EvtQuit, quitSignal{code, nil})
+		t.running.Set(false)
+		t.killPreview()
+		cancel()
 	}()
 
 	looping := true
@@ -3256,8 +3315,16 @@ func (t *Terminal) Loop() {
 	barrier := make(chan bool)
 	go func() {
 		for {
-			<-barrier
-			t.eventChan <- t.tui.GetChar()
+			select {
+			case <-ctx.Done():
+				return
+			case <-barrier:
+			}
+			select {
+			case <-ctx.Done():
+				return
+			case t.eventChan <- t.tui.GetChar():
+			}
 		}
 	}()
 	previewDraggingPos := -1
@@ -3353,7 +3420,7 @@ func (t *Terminal) Loop() {
 				t.pressed = ret
 				t.reqBox.Set(reqClose, nil)
 				t.mutex.Unlock()
-				return
+				return nil
 			}
 		}
 
@@ -3362,8 +3429,7 @@ func (t *Terminal) Loop() {
 		}
 
 		var doAction func(*action) bool
-		var doActions func(actions []*action) bool
-		doActions = func(actions []*action) bool {
+		doActions := func(actions []*action) bool {
 			for iter := 0; iter <= maxFocusEvents; iter++ {
 				currentIndex := t.currentIndex()
 				for _, action := range actions {
@@ -3433,7 +3499,7 @@ func (t *Terminal) Loop() {
 						if valid {
 							t.cancelPreview()
 							t.previewBox.Set(reqPreviewEnqueue,
-								previewRequest{t.previewOpts.command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list})
+								previewRequest{t.previewOpts.command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list, t.environ()})
 						}
 					} else {
 						// Discard the preview content so that it won't accidentally appear
@@ -3547,8 +3613,9 @@ func (t *Terminal) Loop() {
 				}
 			case actTransform:
 				body := t.executeCommand(a.a, false, true, true, false)
-				actions := parseSingleActionList(strings.Trim(body, "\r\n"), func(message string) {})
-				return doActions(actions)
+				if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil {
+					return doActions(actions)
+				}
 			case actTransformBorderLabel:
 				label := t.executeCommand(a.a, false, true, true, true)
 				t.borderLabelOpts.label = label
@@ -3580,6 +3647,8 @@ func (t *Terminal) Loop() {
 					t.input = current.text.ToRunes()
 					t.cx = len(t.input)
 				}
+			case actFatal:
+				req(reqFatal)
 			case actAbort:
 				req(reqQuit)
 			case actDeleteChar:
@@ -4005,10 +4074,10 @@ func (t *Terminal) Loop() {
 				}
 
 				if me.Down {
-					mx_cons := util.Constrain(mx-t.promptLen, 0, len(t.input))
-					if my == t.promptLine() && mx_cons >= 0 {
+					mxCons := util.Constrain(mx-t.promptLen, 0, len(t.input))
+					if my == t.promptLine() && mxCons >= 0 {
 						// Prompt
-						t.cx = mx_cons + t.xoffset
+						t.cx = mxCons + t.xoffset
 					} else if my >= min {
 						t.vset(t.offset + my - min)
 						req(reqList)
@@ -4066,15 +4135,17 @@ func (t *Terminal) Loop() {
 					t.reading = true
 				}
 			case actUnbind:
-				keys := parseKeyChords(a.a, "PANIC")
-				for key := range keys {
-					delete(t.keymap, key)
+				if keys, err := parseKeyChords(a.a, "PANIC"); err == nil {
+					for key := range keys {
+						delete(t.keymap, key)
+					}
 				}
 			case actRebind:
-				keys := parseKeyChords(a.a, "PANIC")
-				for key := range keys {
-					if originalAction, found := t.keymapOrg[key]; found {
-						t.keymap[key] = originalAction
+				if keys, err := parseKeyChords(a.a, "PANIC"); err == nil {
+					for key := range keys {
+						if originalAction, found := t.keymapOrg[key]; found {
+							t.keymap[key] = originalAction
+						}
 					}
 				}
 			case actChangePreview:
@@ -4221,6 +4292,7 @@ func (t *Terminal) Loop() {
 			t.reqBox.Set(event, nil)
 		}
 	}
+	return nil
 }
 
 func (t *Terminal) constrain() {
diff --git a/src/tui/dummy.go b/src/tui/dummy.go
index 7760a724..1a761460 100644
--- a/src/tui/dummy.go
+++ b/src/tui/dummy.go
@@ -8,7 +8,7 @@ func HasFullscreenRenderer() bool {
 	return false
 }
 
-var DefaultBorderShape BorderShape = BorderRounded
+var DefaultBorderShape = BorderRounded
 
 func (a Attr) Merge(b Attr) Attr {
 	return a | b
@@ -29,7 +29,7 @@ const (
 	StrikeThrough = Attr(1 << 7)
 )
 
-func (r *FullscreenRenderer) Init()                              {}
+func (r *FullscreenRenderer) Init() error                        { return nil }
 func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
 func (r *FullscreenRenderer) Pause(bool)                         {}
 func (r *FullscreenRenderer) Resume(bool, bool)                  {}
diff --git a/src/tui/eventtype_string.go b/src/tui/eventtype_string.go
index 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..3ef8b60e 100644
--- a/src/tui/light.go
+++ b/src/tui/light.go
@@ -2,6 +2,7 @@ package tui
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"os"
 	"regexp"
@@ -10,6 +11,7 @@ import (
 	"time"
 	"unicode/utf8"
 
+	"github.com/junegunn/fzf/src/util"
 	"github.com/rivo/uniseg"
 
 	"golang.org/x/term"
@@ -27,8 +29,8 @@ const (
 
 const consoleDevice string = "/dev/tty"
 
-var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
-var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
+var offsetRegexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
+var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
 
 func (r *LightRenderer) PassThrough(str string) {
 	r.queued.WriteString("\x1b7" + str + "\x1b8")
@@ -78,6 +80,7 @@ func (r *LightRenderer) flush() {
 
 // Light renderer
 type LightRenderer struct {
+	closed        *util.AtomicBool
 	theme         *ColorTheme
 	mouse         bool
 	forceBlack    bool
@@ -123,19 +126,24 @@ type LightWindow struct {
 	bg       Color
 }
 
-func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer {
+func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
+	in, err := openTtyIn()
+	if err != nil {
+		return nil, err
+	}
 	r := LightRenderer{
+		closed:        util.NewAtomicBool(false),
 		theme:         theme,
 		forceBlack:    forceBlack,
 		mouse:         mouse,
 		clearOnExit:   clearOnExit,
-		ttyin:         openTtyIn(),
+		ttyin:         in,
 		yoffset:       0,
 		tabstop:       tabstop,
 		fullscreen:    fullscreen,
 		upOneLine:     false,
 		maxHeightFunc: maxHeightFunc}
-	return &r
+	return &r, nil
 }
 
 func repeat(r rune, times int) string {
@@ -153,11 +161,11 @@ func atoi(s string, defaultValue int) int {
 	return value
 }
 
-func (r *LightRenderer) Init() {
+func (r *LightRenderer) Init() error {
 	r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
 
 	if err := r.initPlatform(); err != nil {
-		errorExit(err.Error())
+		return err
 	}
 	r.updateTerminalSize()
 	initTheme(r.theme, r.defaultTheme(), r.forceBlack)
@@ -195,6 +203,7 @@ func (r *LightRenderer) Init() {
 	if !r.fullscreen && r.mouse {
 		r.yoffset, _ = r.findOffset()
 	}
+	return nil
 }
 
 func (r *LightRenderer) Resize(maxHeightFunc func(int) int) {
@@ -233,15 +242,16 @@ func getEnv(name string, defaultValue int) int {
 	return atoi(env, defaultValue)
 }
 
-func (r *LightRenderer) getBytes() []byte {
-	return r.getBytesInternal(r.buffer, false)
+func (r *LightRenderer) getBytes() ([]byte, error) {
+	bytes, err := r.getBytesInternal(r.buffer, false)
+	return bytes, err
 }
 
-func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
+func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte, error) {
 	c, ok := r.getch(nonblock)
 	if !nonblock && !ok {
 		r.Close()
-		errorExit("Failed to read " + consoleDevice)
+		return nil, errors.New("failed to read " + consoleDevice)
 	}
 
 	retries := 0
@@ -272,19 +282,23 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
 		// so terminate fzf immediately.
 		if len(buffer) > maxInputBuffer {
 			r.Close()
-			panic(fmt.Sprintf("Input buffer overflow (%d): %v", len(buffer), buffer))
+			return nil, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer)
 		}
 	}
 
-	return buffer
+	return buffer, nil
 }
 
 func (r *LightRenderer) GetChar() Event {
+	var err error
 	if len(r.buffer) == 0 {
-		r.buffer = r.getBytes()
+		r.buffer, err = r.getBytes()
+		if err != nil {
+			return Event{Fatal, 0, nil}
+		}
 	}
 	if len(r.buffer) == 0 {
-		panic("Empty buffer")
+		return Event{Fatal, 0, nil}
 	}
 
 	sz := 1
@@ -315,7 +329,9 @@ func (r *LightRenderer) GetChar() Event {
 		ev := r.escSequence(&sz)
 		// Second chance
 		if ev.Type == Invalid {
-			r.buffer = r.getBytes()
+			if r.buffer, err = r.getBytes(); err != nil {
+				return Event{Fatal, 0, nil}
+			}
 			ev = r.escSequence(&sz)
 		}
 		return ev
@@ -738,6 +754,7 @@ func (r *LightRenderer) Close() {
 	r.flush()
 	r.closePlatform()
 	r.restoreTerminal()
+	r.closed.Set(true)
 }
 
 func (r *LightRenderer) Top() int {
diff --git a/src/tui/light_unix.go b/src/tui/light_unix.go
index 55e2b246..a5499a00 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..6212378b 100644
--- a/src/util/atexit.go
+++ b/src/util/atexit.go
@@ -1,7 +1,6 @@
 package util
 
 import (
-	"os"
 	"sync"
 )
 
@@ -25,14 +24,5 @@ func RunAtExitFuncs() {
 	for i := len(fns) - 1; i >= 0; i-- {
 		fns[i]()
 	}
-}
-
-// Exit executes any functions registered with AtExit() then exits the program
-// with os.Exit(code).
-//
-// NOTE: It must be used instead of os.Exit() since calling os.Exit() terminates
-// the program before any of the AtExit functions can run.
-func Exit(code int) {
-	defer os.Exit(code)
-	RunAtExitFuncs()
+	atExitFuncs = nil
 }
diff --git a/src/util/util_unix.go b/src/util/util_unix.go
index 4410a9bf..5a67066b 100644
--- a/src/util/util_unix.go
+++ b/src/util/util_unix.go
@@ -61,7 +61,7 @@ func (x *Executor) Become(stdin *os.File, environ []string, command string) {
 	shellPath, err := exec.LookPath(x.shell)
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
-		Exit(127)
+		os.Exit(127)
 	}
 	args := append([]string{shellPath}, append(x.args, command)...)
 	SetStdin(stdin)
diff --git a/src/util/util_windows.go b/src/util/util_windows.go
index 7bbf6ee7..f29e33be 100644
--- a/src/util/util_windows.go
+++ b/src/util/util_windows.go
@@ -97,15 +97,15 @@ func (x *Executor) Become(stdin *os.File, environ []string, command string) {
 	err := cmd.Start()
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
-		Exit(127)
+		os.Exit(127)
 	}
 	err = cmd.Wait()
 	if err != nil {
 		if exitError, ok := err.(*exec.ExitError); ok {
-			Exit(exitError.ExitCode())
+			os.Exit(exitError.ExitCode())
 		}
 	}
-	Exit(0)
+	os.Exit(0)
 }
 
 func escapeArg(s string) string {


Test patch:
diff --git a/main_test.go b/main_test.go
deleted file mode 100644
index 763b2821..00000000
--- a/main_test.go
+++ /dev/null
@@ -1,174 +0,0 @@
-package main
-
-import (
-	"bytes"
-	"fmt"
-	"go/ast"
-	"go/build"
-	"go/importer"
-	"go/parser"
-	"go/token"
-	"go/types"
-	"io/fs"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"sort"
-	"strings"
-	"testing"
-)
-
-func loadPackages(t *testing.T) []*build.Package {
-	// If GOROOT is not set, use `go env GOROOT` to determine it since it
-	// performs more work than just runtime.GOROOT(). For context, running
-	// the tests with the "-trimpath" flag causes GOROOT to not be set.
-	ctxt := &build.Default
-	if ctxt.GOROOT == "" {
-		cmd := exec.Command("go", "env", "GOROOT")
-		out, err := cmd.CombinedOutput()
-		out = bytes.TrimSpace(out)
-		if err != nil {
-			t.Fatalf("error running command: %q: %v\n%s", cmd.Args, err, out)
-		}
-		ctxt.GOROOT = string(out)
-	}
-
-	wd, err := os.Getwd()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	var pkgs []*build.Package
-	seen := make(map[string]bool)
-	err = filepath.WalkDir(wd, func(path string, d fs.DirEntry, err error) error {
-		if err != nil {
-			return err
-		}
-		name := d.Name()
-		if d.IsDir() {
-			if name == "" || name[0] == '.' || name[0] == '_' || name == "vendor" || name == "tmp" {
-				return filepath.SkipDir
-			}
-			return nil
-		}
-		if d.Type().IsRegular() && filepath.Ext(name) == ".go" && !strings.HasSuffix(name, "_test.go") {
-			dir := filepath.Dir(path)
-			if !seen[dir] {
-				pkg, err := ctxt.ImportDir(dir, build.ImportComment)
-				if err != nil {
-					return fmt.Errorf("%s: %s", dir, err)
-				}
-				if pkg.ImportPath == "" || pkg.ImportPath == "." {
-					importPath, err := filepath.Rel(wd, dir)
-					if err != nil {
-						t.Fatal(err)
-					}
-					pkg.ImportPath = filepath.ToSlash(filepath.Join("github.com/junegunn/fzf", importPath))
-				}
-
-				pkgs = append(pkgs, pkg)
-				seen[dir] = true
-			}
-		}
-		return nil
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	sort.Slice(pkgs, func(i, j int) bool {
-		return pkgs[i].ImportPath < pkgs[j].ImportPath
-	})
-	return pkgs
-}
-
-var sourceImporter = importer.ForCompiler(token.NewFileSet(), "source", nil)
-
-func checkPackageForOsExit(t *testing.T, bpkg *build.Package, allowed map[string]int) (errOsExit bool) {
-	var files []*ast.File
-	fset := token.NewFileSet()
-	for _, name := range bpkg.GoFiles {
-		filename := filepath.Join(bpkg.Dir, name)
-		af, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
-		if err != nil {
-			t.Fatal(err)
-		}
-		files = append(files, af)
-	}
-
-	info := types.Info{
-		Uses: make(map[*ast.Ident]types.Object),
-	}
-	conf := types.Config{
-		Importer: sourceImporter,
-	}
-	_, err := conf.Check(bpkg.Name, fset, files, &info)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	wd, err := os.Getwd()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	for id, obj := range info.Uses {
-		if obj.Pkg() != nil && obj.Pkg().Name() == "os" && obj.Name() == "Exit" {
-			pos := fset.Position(id.Pos())
-
-			name, err := filepath.Rel(wd, pos.Filename)
-			if err != nil {
-				t.Log(err)
-				name = pos.Filename
-			}
-			name = filepath.ToSlash(name)
-
-			// Check if the usage is allowed
-			if allowed[name] > 0 {
-				allowed[name]--
-				continue
-			}
-
-			t.Errorf("os.Exit referenced at: %s:%d:%d", name, pos.Line, pos.Column)
-			errOsExit = true
-		}
-	}
-	return errOsExit
-}
-
-// Enforce that src/util.Exit() is used instead of os.Exit by prohibiting
-// references to it anywhere else in the fzf code base.
-func TestOSExitNotAllowed(t *testing.T) {
-	if testing.Short() {
-		t.Skip("skipping: short test")
-	}
-	allowed := map[string]int{
-		"src/util/atexit.go": 1, // os.Exit allowed 1 time in "atexit.go"
-	}
-	var errOsExit bool
-	for _, pkg := range loadPackages(t) {
-		t.Run(pkg.ImportPath, func(t *testing.T) {
-			if checkPackageForOsExit(t, pkg, allowed) {
-				errOsExit = true
-			}
-		})
-	}
-	if t.Failed() && errOsExit {
-		var names []string
-		for name := range allowed {
-			names = append(names, fmt.Sprintf("%q", name))
-		}
-		sort.Strings(names)
-
-		const errMsg = `
-Test failed because os.Exit was referenced outside of the following files:
-
-    %s
-
-Use github.com/junegunn/fzf/src/util.Exit() instead to exit the program.
-This is enforced because calling os.Exit() prevents the functions
-registered with util.AtExit() from running.`
-
-		t.Errorf(errMsg, strings.Join(names, "\n    "))
-	}
-}
diff --git a/src/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/pattern_test.go b/src/pattern_test.go
index 5eb5f6d7..9f105f6a 100644
--- a/src/pattern_test.go
+++ b/src/pattern_test.go
@@ -64,10 +64,15 @@ func TestParseTermsEmpty(t *testing.T) {
 	}
 }
 
+func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
+	withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
+	return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
+		fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
+		withPos, cacheable, nth, delimiter, runes)
+}
+
 func TestExact(t *testing.T) {
-	defer clearPatternCache()
-	clearPatternCache()
-	pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
+	pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
 		[]Range{}, Delimiter{}, []rune("'abc"))
 	chars := util.ToChars([]byte("aabbcc abc"))
 	res, pos := algo.ExactMatchNaive(
@@ -81,9 +86,7 @@ func TestExact(t *testing.T) {
 }
 
 func TestEqual(t *testing.T) {
-	defer clearPatternCache()
-	clearPatternCache()
-	pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
+	pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
 
 	match := func(str string, sidxExpected int, eidxExpected int) {
 		chars := util.ToChars([]byte(str))
@@ -104,19 +107,12 @@ func TestEqual(t *testing.T) {
 }
 
 func TestCaseSensitivity(t *testing.T) {
-	defer clearPatternCache()
-	clearPatternCache()
-	pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
-	clearPatternCache()
-	pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
-	clearPatternCache()
-	pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
-	clearPatternCache()
-	pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
-	clearPatternCache()
-	pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
-	clearPatternCache()
-	pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
+	pat1 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
+	pat2 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
+	pat3 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
+	pat4 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
+	pat5 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
+	pat6 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
 
 	if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
 		string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@@ -129,7 +125,7 @@ func TestCaseSensitivity(t *testing.T) {
 }
 
 func TestOrigTextAndTransformed(t *testing.T) {
-	pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg"))
+	pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg"))
 	tokens := Tokenize("junegunn", Delimiter{})
 	trans := Transform(tokens, []Range{{1, 1}})
 
@@ -163,15 +159,13 @@ func TestOrigTextAndTransformed(t *testing.T) {
 
 func TestCacheKey(t *testing.T) {
 	test := func(extended bool, patStr string, expected string, cacheable bool) {
-		clearPatternCache()
-		pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
+		pat := buildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
 		if pat.CacheKey() != expected {
 			t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
 		}
 		if pat.cacheable != cacheable {
 			t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr)
 		}
-		clearPatternCache()
 	}
 	test(false, "foo !bar", "foo !bar", true)
 	test(false, "foo | bar !baz", "foo | bar !baz", true)
@@ -187,15 +181,13 @@ func TestCacheKey(t *testing.T) {
 
 func TestCacheable(t *testing.T) {
 	test := func(fuzzy bool, str string, expected string, cacheable bool) {
-		clearPatternCache()
-		pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
+		pat := buildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
 		if pat.CacheKey() != expected {
 			t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
 		}
 		if cacheable != pat.cacheable {
 			t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable)
 		}
-		clearPatternCache()
 	}
 	test(true, "foo bar", "foo\tbar", true)
 	test(true, "foo 'bar", "foo\tbar", false)
diff --git a/src/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/test/test_go.rb b/test/test_go.rb
index d3e6b475..5563c080 100755
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -557,7 +557,7 @@ class TestGoFZF < TestBase
 
   def test_expect
     test = lambda do |key, feed, expected = key|
-      tmux.send_keys "seq 1 100 | #{fzf(:expect, key)}", :Enter
+      tmux.send_keys "seq 1 100 | #{fzf(:expect, key, :prompt, "[#{key}]")}", :Enter
       tmux.until { |lines| assert_equal '  100/100', lines[-2] }
       tmux.send_keys '55'
       tmux.until { |lines| assert_equal '  1/100', lines[-2] }