mirror of
https://github.com/peco/peco.git
synced 2026-09-10 07:16:29 -04:00
Replace raw closure callbacks with interfaces
This commit is contained in:
parent
4d178a4675
commit
1fea5cd5a0
|
|
@ -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
|
||||
|
|
|
|||
33
config.go
33
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
76
filter.go
76
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}...)
|
||||
|
|
|
|||
46
layout.go
46
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() {
|
||||
|
|
|
|||
|
|
@ -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
30
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue