summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/actiontype_string.go167
-rw-r--r--src/algo/algo.go2
-rw-r--r--src/algo/normalize.go2
-rw-r--r--src/ansi.go4
-rw-r--r--src/cache.go10
-rw-r--r--src/constants.go10
-rw-r--r--src/core.go68
-rw-r--r--src/matcher.go20
-rw-r--r--src/options.go965
-rw-r--r--src/options_no_pprof.go4
-rw-r--r--src/options_test.go54
-rw-r--r--src/pattern.go34
-rw-r--r--src/pattern_test.go44
-rw-r--r--src/protector/protector.go4
-rw-r--r--src/reader.go19
-rw-r--r--src/server.go27
-rw-r--r--src/terminal.go210
-rw-r--r--src/tokenizer_test.go9
-rw-r--r--src/tui/dummy.go4
-rw-r--r--src/tui/eventtype_string.go51
-rw-r--r--src/tui/light.go49
-rw-r--r--src/tui/light_unix.go18
-rw-r--r--src/tui/light_windows.go6
-rw-r--r--src/tui/tcell.go18
-rw-r--r--src/tui/tui.go10
-rw-r--r--src/util/atexit.go12
-rw-r--r--src/util/util_unix.go2
-rw-r--r--src/util/util_windows.go6
28 files changed, 1118 insertions, 711 deletions
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/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.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/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/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/tokenizer_test.go b/src/tokenizer_test.go
index 985cef9f..3119f797 100644
--- a/src/tokenizer_test.go
+++ b/src/tokenizer_test.go
@@ -71,14 +71,14 @@ func TestTransform(t *testing.T) {
{
tokens := Tokenize(input, Delimiter{})
{
- ranges := splitNth("1,2,3")
+ ranges, _ := splitNth("1,2,3")
tx := Transform(tokens, ranges)
if joinTokens(tx) != "abc: def: ghi: " {
t.Errorf("%s", tx)
}
}
{
- ranges := splitNth("1..2,3,2..,1")
+ ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
len(tx) != 4 ||
@@ -93,7 +93,7 @@ func TestTransform(t *testing.T) {
{
tokens := Tokenize(input, delimiterRegexp(":"))
{
- ranges := splitNth("1..2,3,2..,1")
+ ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges)
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx) != 4 ||
@@ -108,5 +108,6 @@ func TestTransform(t *testing.T) {
}
func TestTransformIndexOutOfBounds(t *testing.T) {
- Transform([]Token{}, splitNth("1"))
+ s, _ := splitNth("1")
+ Transform([]Token{}, s)
}
diff --git a/src/tui/dummy.go b/src/tui/dummy.go
index 7760a724..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 {