Replace raw closure callbacks with interfaces

This commit is contained in:
Daisuke Maki 2026-02-20 13:35:31 +09:00
parent 4d178a4675
commit 1fea5cd5a0
13 changed files with 178 additions and 96 deletions

View file

@ -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

View file

@ -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
}
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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])
}

View file

@ -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}...)

View file

@ -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() {

View file

@ -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,

30
peco.go
View file

@ -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.

View file

@ -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
}