diff --git a/action_test.go b/action_test.go index 24d7590..3d7e7e3 100644 --- a/action_test.go +++ b/action_test.go @@ -104,7 +104,7 @@ func (b *failingBuffer) Size() int { func TestDoSelectAllWithLineAtError(t *testing.T) { state := New() state.screen = NewDummyScreen() - state.readConfigFn = func(*Config, string) error { return nil } + state.configReader = nopConfigReader rh := &recordingHub{} state.hub = rh @@ -135,7 +135,7 @@ func TestDoSelectAllWithLineAtError(t *testing.T) { func TestDoInvertSelectionWithLineAtError(t *testing.T) { state := New() state.screen = NewDummyScreen() - state.readConfigFn = func(*Config, string) error { return nil } + state.configReader = nopConfigReader rh := &recordingHub{} state.hub = rh diff --git a/config.go b/config.go index 1080869..1f8aab4 100644 --- a/config.go +++ b/config.go @@ -312,15 +312,24 @@ func stringsToStyle(style *Style, raw []string) error { return nil } -// This is a variable because we want to change its behavior -// when we run tests. -type configLocateFunc func(string) (string, error) +// ConfigLocator locates a config file in a given directory. +type ConfigLocator interface { + Locate(string) (string, error) +} + +// ConfigLocatorFunc is a function that implements ConfigLocator. +type ConfigLocatorFunc func(string) (string, error) + +// Locate calls the underlying function. +func (f ConfigLocatorFunc) Locate(dir string) (string, error) { + return f(dir) +} var configFilenames = []string{"config.json", "config.yaml", "config.yml"} -// locateRcfileIn searches the given directory for a config file with one of -// the known filenames (config.json, config.yaml, config.yml). -func locateRcfileIn(dir string) (string, error) { +// defaultConfigLocator searches for a config file with one of the known +// filenames (config.json, config.yaml, config.yml) in the given directory. +var defaultConfigLocator = ConfigLocatorFunc(func(dir string) (string, error) { for _, basename := range configFilenames { file := filepath.Join(dir, basename) if _, err := os.Stat(file); err == nil { @@ -328,10 +337,10 @@ func locateRcfileIn(dir string) (string, error) { } } return "", fmt.Errorf("config file not found in %s", dir) -} +}) // LocateRcfile attempts to find the config file in various locations -func LocateRcfile(locater configLocateFunc) (string, error) { +func LocateRcfile(locater ConfigLocator) (string, error) { // http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html // // Try in this order: @@ -343,12 +352,12 @@ func LocateRcfile(locater configLocateFunc) (string, error) { // Try dir supplied via env var if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" { - if file, err := locater(filepath.Join(dir, "peco")); err == nil { + if file, err := locater.Locate(filepath.Join(dir, "peco")); err == nil { return file, nil } } else if uErr == nil { // silently ignore failure for homedir() // Try "default" XDG location, is user is available - if file, err := locater(filepath.Join(home, ".config", "peco")); err == nil { + if file, err := locater.Locate(filepath.Join(home, ".config", "peco")); err == nil { return file, nil } } @@ -358,14 +367,14 @@ func LocateRcfile(locater configLocateFunc) (string, error) { // with filepath.ListSeparator, so use it if dirs := os.Getenv("XDG_CONFIG_DIRS"); dirs != "" { for dir := range strings.SplitSeq(dirs, fmt.Sprintf("%c", filepath.ListSeparator)) { - if file, err := locater(filepath.Join(dir, "peco")); err == nil { + if file, err := locater.Locate(filepath.Join(dir, "peco")); err == nil { return file, nil } } } if uErr == nil { // silently ignore failure for homedir() - if file, err := locater(filepath.Join(home, ".peco")); err == nil { + if file, err := locater.Locate(filepath.Join(home, ".peco")); err == nil { return file, nil } } diff --git a/config_test.go b/config_test.go index 2327fe6..07dcc71 100644 --- a/config_test.go +++ b/config_test.go @@ -172,13 +172,13 @@ func TestLocateRcfile(t *testing.T) { } i := 0 - locater := func(dir string) (string, error) { + locater := ConfigLocatorFunc(func(dir string) (string, error) { t.Logf("looking for file in %s", dir) require.True(t, i <= len(expected)-1, "Got %d directories, only have %d", i+1, len(expected)) require.Equal(t, expected[i], dir, "Expected %s, got %s", expected[i], dir) i++ return "", errors.New("error: Not found") - } + }) t.Setenv("XDG_CONFIG_HOME", dir) t.Setenv("XDG_CONFIG_DIRS", strings.Join( @@ -213,7 +213,7 @@ func TestLocateRcfileYAML(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", "") t.Setenv("XDG_CONFIG_DIRS", "") - file, err := LocateRcfile(locateRcfileIn) + file, err := LocateRcfile(defaultConfigLocator) require.NoError(t, err) require.Equal(t, filepath.Join(pecoDir, "config.yaml"), file) } diff --git a/filter.go b/filter.go index 0caf6c5..b36d56c 100644 --- a/filter.go +++ b/filter.go @@ -15,6 +15,22 @@ import ( "github.com/peco/peco/pipeline" ) +// FilterErrorHandler handles errors that occur during filter execution. +type FilterErrorHandler interface { + HandleError(error) +} + +// FilterErrorHandlerFunc is a function that implements FilterErrorHandler. +type FilterErrorHandlerFunc func(error) + +// HandleError calls the underlying function. +func (f FilterErrorHandlerFunc) HandleError(err error) { + f(err) +} + +// nopFilterErrorHandler is a FilterErrorHandler that silently discards errors. +var nopFilterErrorHandler = FilterErrorHandlerFunc(func(error) {}) + // Filter is responsible for the actual "grep" part of peco type Filter struct { state *Peco @@ -26,26 +42,26 @@ type Filter struct { } type filterProcessor struct { - filter filter.Filter - query string - bufSize int - onError func(error) + filter filter.Filter + query string + bufSize int + errorHandler FilterErrorHandler } // newFilterProcessor creates a filterProcessor that runs the given filter // against a query, managing result buffering and error reporting. -func newFilterProcessor(f filter.Filter, q string, bufSize int, onError func(error)) *filterProcessor { +func newFilterProcessor(f filter.Filter, q string, bufSize int, eh FilterErrorHandler) *filterProcessor { return &filterProcessor{ - filter: f, - query: q, - bufSize: bufSize, - onError: onError, + filter: f, + query: q, + bufSize: bufSize, + errorHandler: eh, } } // Accept receives lines from the pipeline, applies the filter, and forwards matches. func (fp *filterProcessor) Accept(ctx context.Context, in <-chan line.Line, out pipeline.ChanOutput) { - acceptAndFilter(ctx, fp.filter, fp.bufSize, fp.onError, in, out) + acceptAndFilter(ctx, fp.filter, fp.bufSize, fp.errorHandler, in, out) } // orderedChunk is a batch of lines tagged with a sequence number @@ -61,18 +77,18 @@ type orderedResult struct { matched []line.Line } -// reportFilterError calls onError with a non-context-cancellation error. -// If the context is already cancelled or onError is nil, it does nothing. -func reportFilterError(ctx context.Context, err error, onError func(error)) { - if err == nil || ctx.Err() != nil || onError == nil { +// reportFilterError reports a filter error to the given handler. +// If the context is already cancelled or err is nil, it does nothing. +func reportFilterError(ctx context.Context, err error, eh FilterErrorHandler) { + if err == nil || ctx.Err() != nil { return } - onError(err) + eh.HandleError(err) } // flusher is the single-threaded fallback used when the filter does not // support parallel execution (e.g. Fuzzy with sortLongest). -func flusher(ctx context.Context, f filter.Filter, incoming chan []line.Line, done chan struct{}, out pipeline.ChanOutput, onError func(error)) { +func flusher(ctx context.Context, f filter.Filter, incoming chan []line.Line, done chan struct{}, out pipeline.ChanOutput, eh FilterErrorHandler) { if pdebug.Enabled { g := pdebug.Marker("flusher goroutine") defer g.End() @@ -93,7 +109,7 @@ func flusher(ctx context.Context, f filter.Filter, incoming chan []line.Line, do pdebug.Printf("flusher: %#v", buf) } if err := f.Apply(ctx, buf, out); err != nil { - reportFilterError(ctx, err, onError) + reportFilterError(ctx, err, eh) } buffer.ReleaseLineListBuf(buf) } @@ -102,7 +118,7 @@ func flusher(ctx context.Context, f filter.Filter, incoming chan []line.Line, do // parallelFlusher distributes filter work across multiple goroutines // and merges the results back in sequence order. -func parallelFlusher(ctx context.Context, f filter.Filter, incoming chan orderedChunk, done chan struct{}, out pipeline.ChanOutput, onError func(error)) { +func parallelFlusher(ctx context.Context, f filter.Filter, incoming chan orderedChunk, done chan struct{}, out pipeline.ChanOutput, eh FilterErrorHandler) { if pdebug.Enabled { g := pdebug.Marker("parallelFlusher goroutine") defer g.End() @@ -142,7 +158,7 @@ func parallelFlusher(ctx context.Context, f filter.Filter, incoming chan ordered var err error matched, err = collector.ApplyCollect(ctx, chunk.lines) if err != nil { - reportFilterError(ctx, err, onError) + reportFilterError(ctx, err, eh) } } else { // Fallback: use channel-based Apply for filters that @@ -150,7 +166,7 @@ func parallelFlusher(ctx context.Context, f filter.Filter, incoming chan ordered collectCh := make(chan line.Line, len(chunk.lines)) go func(chunk orderedChunk) { if err := f.Apply(ctx, chunk.lines, pipeline.ChanOutput(collectCh)); err != nil { - reportFilterError(ctx, err, onError) + reportFilterError(ctx, err, eh) } close(collectCh) }(chunk) @@ -235,12 +251,12 @@ func parallelFlusher(ctx context.Context, f filter.Filter, incoming chan ordered // It batches incoming lines and dispatches them to the filter, using parallel // workers when the filter supports it. func AcceptAndFilter(ctx context.Context, f filter.Filter, configBufSize int, in <-chan line.Line, out pipeline.ChanOutput) { - acceptAndFilter(ctx, f, configBufSize, nil, in, out) + acceptAndFilter(ctx, f, configBufSize, nopFilterErrorHandler, in, out) } // acceptAndFilter is the core filtering loop: it reads lines from in, batches // them, applies the filter function, and buffers matches for output. -func acceptAndFilter(ctx context.Context, f filter.Filter, configBufSize int, onError func(error), in <-chan line.Line, out pipeline.ChanOutput) { +func acceptAndFilter(ctx context.Context, f filter.Filter, configBufSize int, eh FilterErrorHandler, in <-chan line.Line, out pipeline.ChanOutput) { useParallel := f.SupportsParallel() && runtime.GOMAXPROCS(0) > 1 buf := buffer.GetLineListBuf() @@ -254,18 +270,18 @@ func acceptAndFilter(ctx context.Context, f filter.Filter, configBufSize int, on } if useParallel { - acceptAndFilterParallel(ctx, f, bufsiz, buf, onError, in, out) + acceptAndFilterParallel(ctx, f, bufsiz, buf, eh, in, out) } else { - acceptAndFilterSerial(ctx, f, bufsiz, buf, onError, in, out) + acceptAndFilterSerial(ctx, f, bufsiz, buf, eh, in, out) } } // acceptAndFilterSerial runs the filter in a single goroutine, used as the // fallback when the filter does not support parallel execution. -func acceptAndFilterSerial(ctx context.Context, f filter.Filter, bufsiz int, buf []line.Line, onError func(error), in <-chan line.Line, out pipeline.ChanOutput) { +func acceptAndFilterSerial(ctx context.Context, f filter.Filter, bufsiz int, buf []line.Line, eh FilterErrorHandler, in <-chan line.Line, out pipeline.ChanOutput) { flush := make(chan []line.Line) flushDone := make(chan struct{}) - go flusher(ctx, f, flush, flushDone, out, onError) + go flusher(ctx, f, flush, flushDone, out, eh) defer func() { <-flushDone }() defer close(flush) @@ -274,10 +290,10 @@ func acceptAndFilterSerial(ctx context.Context, f filter.Filter, bufsiz int, buf // acceptAndFilterParallel distributes filter work across multiple goroutines, // tagging each batch with a sequence number for ordered result merging. -func acceptAndFilterParallel(ctx context.Context, f filter.Filter, bufsiz int, buf []line.Line, onError func(error), in <-chan line.Line, out pipeline.ChanOutput) { +func acceptAndFilterParallel(ctx context.Context, f filter.Filter, bufsiz int, buf []line.Line, eh FilterErrorHandler, in <-chan line.Line, out pipeline.ChanOutput) { flush := make(chan orderedChunk) flushDone := make(chan struct{}) - go parallelFlusher(ctx, f, flush, flushDone, out, onError) + go parallelFlusher(ctx, f, flush, flushDone, out, eh) defer func() { <-flushDone }() defer close(flush) @@ -450,9 +466,9 @@ func (f *Filter) Work(ctx context.Context, q *hub.Payload[string]) { ctx = selectedFilter.NewContext(ctx, query) // Report non-cancellation filter errors (e.g. regex compilation failures) // to the status bar so the user can see why results are missing. - onFilterError := func(err error) { + onFilterError := FilterErrorHandlerFunc(func(err error) { state.Hub().SendStatusMsg(ctx, err.Error(), 5*time.Second) - } + }) p.Add(newFilterProcessor(selectedFilter, query, state.config.FilterBufSize, onFilterError)) buf := NewMemoryBuffer(srcSize / 4) diff --git a/filter/base.go b/filter/base.go index 76ceedb..a4afca2 100644 --- a/filter/base.go +++ b/filter/base.go @@ -7,11 +7,46 @@ import ( "github.com/peco/peco/pipeline" ) +// LineEmitter receives matched lines from a filter. +type LineEmitter interface { + Emit(line.Line) +} + +// chanEmitter sends matched lines to a pipeline channel. +type chanEmitter struct { + ctx context.Context + out pipeline.ChanOutput +} + +func (e *chanEmitter) Emit(l line.Line) { + _ = e.out.Send(e.ctx, l) +} + +// LineCollector accumulates matched lines into a slice. +type LineCollector struct { + lines []line.Line +} + +// NewLineCollector creates a LineCollector pre-allocated with the given capacity. +func NewLineCollector(cap int) *LineCollector { + return &LineCollector{lines: make([]line.Line, 0, cap)} +} + +// Emit appends a matched line to the collector. +func (c *LineCollector) Emit(l line.Line) { + c.lines = append(c.lines, l) +} + +// Lines returns the accumulated matched lines. +func (c *LineCollector) Lines() []line.Line { + return c.lines +} + // baseFilter provides shared implementations of Apply, ApplyCollect, // NewContext, and BufSize for filters that follow the applyInternal pattern. // Filters embed this type and set applyFn to their type-specific matching logic. type baseFilter struct { - applyFn func(ctx context.Context, lines []line.Line, emit func(line.Line)) error + applyFn func(ctx context.Context, lines []line.Line, em LineEmitter) error } // NewContext returns a context initialized with the given query for pipeline use. @@ -25,17 +60,13 @@ func (b *baseFilter) BufSize() int { // Apply runs the filter's matching logic on lines, sending matches to out. func (b *baseFilter) Apply(ctx context.Context, lines []line.Line, out pipeline.ChanOutput) error { - return b.applyFn(ctx, lines, func(l line.Line) { - _ = out.Send(ctx, l) - }) + return b.applyFn(ctx, lines, &chanEmitter{ctx: ctx, out: out}) } // ApplyCollect runs the filter and returns matched lines directly as a slice, // bypassing channel-based output for better performance in parallel paths. func (b *baseFilter) ApplyCollect(ctx context.Context, lines []line.Line) ([]line.Line, error) { - result := make([]line.Line, 0, len(lines)/2) - err := b.applyFn(ctx, lines, func(l line.Line) { - result = append(result, l) - }) - return result, err + c := NewLineCollector(len(lines) / 2) + err := b.applyFn(ctx, lines, c) + return c.Lines(), err } diff --git a/filter/fuzzy.go b/filter/fuzzy.go index 3bdfb26..ea4d443 100644 --- a/filter/fuzzy.go +++ b/filter/fuzzy.go @@ -48,7 +48,7 @@ func (ff Fuzzy) String() string { // applyInternal performs fuzzy matching on each line, emitting matches with // their character-level match indices for highlighting. -func (ff *Fuzzy) applyInternal(ctx context.Context, lines []line.Line, emit func(line.Line)) error { +func (ff *Fuzzy) applyInternal(ctx context.Context, lines []line.Line, em LineEmitter) error { originalQuery := pipeline.QueryFromContext(ctx) // Parse negative terms and compile them as case-insensitive regexps @@ -83,7 +83,7 @@ LINE: // All-negative query: emit all non-excluded lines with nil indices if len(fuzzyQuery) == 0 { - emit(line.NewMatched(l, nil)) + em.Emit(line.NewMatched(l, nil)) continue LINE } @@ -179,7 +179,7 @@ LINE: } for i := range matched { - emit(line.NewMatched(matched[i].line, matched[i].matches)) + em.Emit(line.NewMatched(matched[i].line, matched[i].matches)) } return nil diff --git a/filter/regexp.go b/filter/regexp.go index bc7a319..e7f78bf 100644 --- a/filter/regexp.go +++ b/filter/regexp.go @@ -196,7 +196,7 @@ func (f *regexpQueryFactory) Compile(s string, flags regexpFlags, quotemeta bool // applyInternal matches each line against the compiled positive and negative // regexps, deduplicating overlapping match ranges before emitting results. -func (rf *Regexp) applyInternal(ctx context.Context, lines []line.Line, emit func(line.Line)) error { +func (rf *Regexp) applyInternal(ctx context.Context, lines []line.Line, em LineEmitter) error { query := pipeline.QueryFromContext(ctx) posRegexps, negRegexps, err := rf.factory.Compile(query, rf.flags, rf.quotemeta) if err != nil { @@ -216,7 +216,7 @@ func (rf *Regexp) applyInternal(ctx context.Context, lines []line.Line, emit fun // All-negative query: emit line with nil indices (no highlighting) if len(posRegexps) == 0 { - emit(line.NewMatched(l, nil)) + em.Emit(line.NewMatched(l, nil)) continue } @@ -265,7 +265,7 @@ func (rf *Regexp) applyInternal(ctx context.Context, lines []line.Line, emit fun deduped = append(deduped, m) } } - emit(line.NewMatched(l, deduped)) + em.Emit(line.NewMatched(l, deduped)) } return nil } diff --git a/filter_incremental_test.go b/filter_incremental_test.go index 2f4aa9f..5c5fa20 100644 --- a/filter_incremental_test.go +++ b/filter_incremental_test.go @@ -369,17 +369,17 @@ func TestFilterApplyErrorReporting(t *testing.T) { var mu sync.Mutex var reported []error - onError := func(err error) { + onError := FilterErrorHandlerFunc(func(err error) { mu.Lock() defer mu.Unlock() reported = append(reported, err) - } + }) out := make(chan line.Line, len(inputLines)) acceptAndFilter(context.Background(), ef, 0, onError, in, pipeline.ChanOutput(out)) mu.Lock() defer mu.Unlock() - require.Len(t, reported, 1, "onError should have been called once") + require.Len(t, reported, 1, "error handler should have been called once") require.Equal(t, simulatedErr, reported[0]) } diff --git a/issues_test.go b/issues_test.go index 8d7e53d..05bc453 100644 --- a/issues_test.go +++ b/issues_test.go @@ -66,7 +66,7 @@ func TestIssue345(t *testing.T) { defer os.Remove(cfg) state := newPeco() - state.readConfigFn = readConfig + state.configReader = defaultConfigReader require.NoError(t, state.config.Init(), "Config.Init should succeed") state.Argv = append(state.Argv, []string{"--rcfile", cfg}...) diff --git a/layout.go b/layout.go index 90d247c..d0c551d 100644 --- a/layout.go +++ b/layout.go @@ -109,22 +109,32 @@ type ansiLiner interface { ANSIAttrs() []ansi.AttrSpan } -// LayoutFactory is a function that creates a BasicLayout for the given Peco state. -type LayoutFactory func(*Peco) (*BasicLayout, error) +// LayoutBuilder creates a BasicLayout for the given Peco state. +type LayoutBuilder interface { + Build(*Peco) (*BasicLayout, error) +} -var layoutRegistry = map[LayoutType]LayoutFactory{} +// LayoutBuilderFunc is a function that implements LayoutBuilder. +type LayoutBuilderFunc func(*Peco) (*BasicLayout, error) -// RegisterLayout registers a layout factory under the given name. -func RegisterLayout(name LayoutType, factory LayoutFactory) { - layoutRegistry[name] = factory +// Build calls the underlying function. +func (f LayoutBuilderFunc) Build(state *Peco) (*BasicLayout, error) { + return f(state) +} + +var layoutRegistry = map[LayoutType]LayoutBuilder{} + +// RegisterLayout registers a layout builder under the given name. +func RegisterLayout(name LayoutType, builder LayoutBuilder) { + layoutRegistry[name] = builder } // NewLayout creates a layout by looking up the registry. Falls back to top-down. func NewLayout(layoutType LayoutType, state *Peco) (*BasicLayout, error) { - if factory, ok := layoutRegistry[layoutType]; ok { - return factory(state) + if builder, ok := layoutRegistry[layoutType]; ok { + return builder.Build(state) } - return layoutRegistry[LayoutTypeTopDown](state) + return layoutRegistry[LayoutTypeTopDown].Build(state) } // IsValidLayoutType checks if a string is a supported layout type @@ -803,8 +813,8 @@ func newStatusBar(state *Peco) (StatusBar, error) { return newScreenStatusBar(state.Screen(), AnchorBottom, 0+extraOffset, state.Styles()) } -// NewDefaultLayout creates a new Layout in the default format (top-down) -func NewDefaultLayout(state *Peco) (*BasicLayout, error) { +// DefaultLayout creates a Layout in the default format (top-down). +func DefaultLayout(state *Peco) (*BasicLayout, error) { sb, err := newStatusBar(state) if err != nil { return nil, err @@ -825,8 +835,8 @@ func NewDefaultLayout(state *Peco) (*BasicLayout, error) { }, nil } -// NewBottomUpLayout creates a new Layout in bottom-up format -func NewBottomUpLayout(state *Peco) (*BasicLayout, error) { +// BottomUpLayout creates a Layout in bottom-up format. +func BottomUpLayout(state *Peco) (*BasicLayout, error) { sb, err := newStatusBar(state) if err != nil { return nil, err @@ -847,9 +857,9 @@ func NewBottomUpLayout(state *Peco) (*BasicLayout, error) { }, nil } -// NewTopDownQueryBottomLayout creates a new Layout with list top-to-bottom +// TopDownQueryBottomLayout creates a Layout with list top-to-bottom // and the query prompt at the bottom. -func NewTopDownQueryBottomLayout(state *Peco) (*BasicLayout, error) { +func TopDownQueryBottomLayout(state *Peco) (*BasicLayout, error) { sb, err := newStatusBar(state) if err != nil { return nil, err @@ -876,9 +886,9 @@ func (l *BasicLayout) SortTopDown() bool { } func init() { - RegisterLayout(LayoutTypeTopDown, NewDefaultLayout) - RegisterLayout(LayoutTypeBottomUp, NewBottomUpLayout) - RegisterLayout(LayoutTypeTopDownQueryBottom, NewTopDownQueryBottomLayout) + RegisterLayout(LayoutTypeTopDown, LayoutBuilderFunc(DefaultLayout)) + RegisterLayout(LayoutTypeBottomUp, LayoutBuilderFunc(BottomUpLayout)) + RegisterLayout(LayoutTypeTopDownQueryBottom, LayoutBuilderFunc(TopDownQueryBottomLayout)) } func (l *BasicLayout) PurgeDisplayCache() { diff --git a/layout_test.go b/layout_test.go index 5a6afc7..166198e 100644 --- a/layout_test.go +++ b/layout_test.go @@ -315,7 +315,7 @@ func TestGHIssue455_DrawScreenForceSync(t *testing.T) { t.Run("ForceSync true calls Sync instead of final Flush", func(t *testing.T) { state, screen := setupState(t) - layout, err := NewDefaultLayout(state) + layout, err := DefaultLayout(state) require.NoError(t, err) screen.reset() @@ -349,7 +349,7 @@ func TestGHIssue455_DrawScreenForceSync(t *testing.T) { t.Run("ForceSync false does not call Sync", func(t *testing.T) { state, screen := setupState(t) - layout, err := NewDefaultLayout(state) + layout, err := DefaultLayout(state) require.NoError(t, err) screen.reset() @@ -361,7 +361,7 @@ func TestGHIssue455_DrawScreenForceSync(t *testing.T) { t.Run("nil options does not call Sync", func(t *testing.T) { state, screen := setupState(t) - layout, err := NewDefaultLayout(state) + layout, err := DefaultLayout(state) require.NoError(t, err) screen.reset() @@ -423,7 +423,7 @@ func TestTopDownQueryBottomLayout(t *testing.T) { state.Filters().Add(filter.NewIgnoreCase()) - layout, err := NewTopDownQueryBottomLayout(state) + layout, err := TopDownQueryBottomLayout(state) require.NoError(t, err) require.Equal(t, AnchorBottom, layout.prompt.anchor, diff --git a/peco.go b/peco.go index ba7dd2d..5563f13 100644 --- a/peco.go +++ b/peco.go @@ -83,7 +83,7 @@ type Peco struct { selectAllAndExit bool // True if --select-all is enabled singleKeyJump SingleKeyJumpState heightSpec *HeightSpec - readConfigFn func(*Config, string) error + configReader ConfigReader styles StyleSet enableANSI bool // Enable ANSI color code support fuzzyLongestSort bool @@ -209,7 +209,7 @@ func New() *Peco { idgen: newIDGen(), queryExec: QueryExecState{delay: 50 * time.Millisecond}, readyCh: make(chan struct{}), - readConfigFn: readConfig, + configReader: defaultConfigReader, screen: NewTcellScreen(), selection: NewSelection(), maxScanBufferSize: bufio.MaxScanTokenSize, @@ -381,7 +381,7 @@ func (p *Peco) Setup() (err error) { } // Read config - if err := p.readConfigFn(&p.config, opts.OptRcfile); err != nil { + if err := p.configReader.ReadConfig(&p.config, opts.OptRcfile); err != nil { return fmt.Errorf("failed to setup configuration: %w", err) } @@ -641,7 +641,7 @@ func (p *Peco) parseCommandLine(opts *CLIOptions, args *[]string, argv []string) } if opts.OptRcfile == "" { - if file, err := LocateRcfile(locateRcfileIn); err == nil { + if file, err := LocateRcfile(defaultConfigLocator); err == nil { opts.OptRcfile = file } } @@ -701,9 +701,25 @@ func (p *Peco) SetupSource(ctx context.Context) (s *Source, err error) { return src, nil } -// readConfig loads the configuration from the given filename into cfg. +// ConfigReader reads configuration from a file into a Config struct. +type ConfigReader interface { + ReadConfig(*Config, string) error +} + +// ConfigReaderFunc is a function that implements ConfigReader. +type ConfigReaderFunc func(*Config, string) error + +// ReadConfig calls the underlying function. +func (f ConfigReaderFunc) ReadConfig(cfg *Config, filename string) error { + return f(cfg, filename) +} + +// nopConfigReader is a ConfigReader that does nothing. +var nopConfigReader = ConfigReaderFunc(func(*Config, string) error { return nil }) + +// defaultConfigReader loads the configuration from the given filename into cfg. // If filename is empty, no file is read and nil is returned. -func readConfig(cfg *Config, filename string) error { +var defaultConfigReader = ConfigReaderFunc(func(cfg *Config, filename string) error { if filename != "" { if err := cfg.ReadFilename(filename); err != nil { return fmt.Errorf("failed to read config file: %w", err) @@ -711,7 +727,7 @@ func readConfig(cfg *Config, filename string) error { } return nil -} +}) // ApplyConfig applies the loaded Config and CLI options to the Peco instance, // setting up layout, styles, keymap, filters, and all other runtime parameters. diff --git a/peco_test.go b/peco_test.go index e592eac..d93700c 100644 --- a/peco_test.go +++ b/peco_test.go @@ -89,7 +89,7 @@ func newPeco() *Peco { state := New() state.Argv = []string{"peco", file} state.screen = NewDummyScreen() - state.readConfigFn = func(*Config, string) error { return nil } + state.configReader = nopConfigReader return state }