This commit is contained in:
Daisuke Maki 2026-02-15 19:42:00 +09:00
parent 7715ff482f
commit ca4a52ba53
3 changed files with 198 additions and 0 deletions

View file

@ -98,6 +98,7 @@ type Peco struct {
selectionRangeStart RangeStart
exitZeroAndExit bool // True if --exit-0 is enabled
selectOneAndExit bool // True if --select-1 is enabled
selectAllAndExit bool // True if --select-all is enabled
singleKeyJumpMode bool
singleKeyJumpPrefixes []rune
singleKeyJumpPrefixMap map[rune]uint
@ -455,6 +456,7 @@ type CLIOptions struct {
OptLayout string `long:"layout" description:"layout to be used. 'top-down' or 'bottom-up'. default is 'top-down'"`
OptSelect1 bool `long:"select-1" description:"select first item and immediately exit if the input contains only 1 item"`
OptExitZero bool `long:"exit-0" description:"exit immediately with status 1 if the input is empty"`
OptSelectAll bool `long:"select-all" description:"select all items and immediately exit"`
OptOnCancel string `long:"on-cancel" description:"specify action on user cancel. 'success' or 'error'.\ndefault is 'success'. This may change in future versions"`
OptSelectionPrefix string `long:"selection-prefix" description:"use a prefix instead of changing line color to indicate currently selected lines.\ndefault is to use colors. This option is experimental"`
OptExec string `long:"exec" description:"execute command instead of finishing/terminating peco.\nPlease note that this command will receive selected line(s) from stdin,\nand will be executed via '/bin/sh -c' or 'cmd /c'"`

23
peco.go
View file

@ -319,6 +319,17 @@ func (p *Peco) exitZeroIfPossible() {
}
}
func (p *Peco) selectAllAndExitIfPossible() {
b := p.CurrentLineBuffer()
selection := p.Selection()
for i := 0; i < b.Size(); i++ {
if l, err := b.LineAt(i); err == nil {
selection.Add(l)
}
}
p.Exit(errCollectResults{})
}
func (p *Peco) Run(ctx context.Context) (err error) {
if pdebug.Enabled {
g := pdebug.Marker("Peco.Run").BindError(&err)
@ -409,6 +420,15 @@ func (p *Peco) Run(ctx context.Context) (err error) {
}()
}
// If --select-all is enabled and there is no query, select all lines
// from the source and exit immediately
if p.selectAllAndExit && p.initialQuery == "" {
go func() {
<-p.source.SetupDone()
p.selectAllAndExitIfPossible()
}()
}
readyOnce.Do(func() { close(p.readyCh) })
// This has tobe AFTER close(p.readyCh), otherwise the query is
@ -426,6 +446,8 @@ func (p *Peco) Run(ctx context.Context) (err error) {
// if we only have one item
if p.selectOneAndExit {
p.ExecQuery(p.selectOneAndExitIfPossible)
} else if p.selectAllAndExit {
p.ExecQuery(p.selectAllAndExitIfPossible)
} else {
p.ExecQuery(nil)
}
@ -576,6 +598,7 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error {
}
p.selectOneAndExit = opts.OptSelect1
p.exitZeroAndExit = opts.OptExitZero
p.selectAllAndExit = opts.OptSelectAll
p.printQuery = opts.OptPrintQuery
p.initialQuery = opts.OptQuery
p.initialFilter = opts.OptInitialFilter

View file

@ -19,6 +19,7 @@ import (
"github.com/peco/peco/internal/util"
"github.com/peco/peco/line"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type nullHub struct{}
@ -334,6 +335,7 @@ func TestApplyConfig(t *testing.T) {
opts.OptLayout = "bottom-up"
opts.OptSelect1 = true
opts.OptExitZero = true
opts.OptSelectAll = true
opts.OptOnCancel = "error"
opts.OptSelectionPrefix = ">"
opts.OptPrintQuery = true
@ -379,6 +381,10 @@ func TestApplyConfig(t *testing.T) {
return
}
if !assert.Equal(t, opts.OptSelectAll, p.selectAllAndExit, "p.selectAllAndExit should be equal to opts.OptSelectAll") {
return
}
if !assert.Equal(t, opts.OptOnCancel, p.onCancel, "p.onCancel should be equal to opts.OptOnCancel") {
return
}
@ -594,6 +600,173 @@ func TestExitZero(t *testing.T) {
})
}
func TestSelectAll(t *testing.T) {
t.Run("Multiple lines outputs all lines", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
p := newPeco()
p.Argv = []string{"--select-all"}
p.Stdin = bytes.NewBufferString("foo\nbar\nbaz\n")
var out bytes.Buffer
p.Stdout = &out
resultCh := make(chan error)
go func() {
defer close(resultCh)
select {
case <-ctx.Done():
return
case resultCh <- p.Run(ctx):
return
}
}()
select {
case <-ctx.Done():
t.Errorf("timeout reached")
return
case err := <-resultCh:
require.True(t, util.IsCollectResultsError(err), "isCollectResultsError")
p.PrintResults()
}
require.Equal(t, "foo\nbar\nbaz\n", out.String(), "output should match")
})
t.Run("Single line outputs that line", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
p := newPeco()
p.Argv = []string{"--select-all"}
p.Stdin = bytes.NewBufferString("only\n")
var out bytes.Buffer
p.Stdout = &out
resultCh := make(chan error)
go func() {
defer close(resultCh)
select {
case <-ctx.Done():
return
case resultCh <- p.Run(ctx):
return
}
}()
select {
case <-ctx.Done():
t.Errorf("timeout reached")
return
case err := <-resultCh:
require.True(t, util.IsCollectResultsError(err), "isCollectResultsError")
p.PrintResults()
}
require.Equal(t, "only\n", out.String(), "output should match")
})
t.Run("Empty input outputs nothing", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
p := newPeco()
p.Argv = []string{"--select-all"}
p.Stdin = bytes.NewBufferString("")
var out bytes.Buffer
p.Stdout = &out
resultCh := make(chan error)
go func() {
defer close(resultCh)
select {
case <-ctx.Done():
return
case resultCh <- p.Run(ctx):
return
}
}()
select {
case <-ctx.Done():
t.Errorf("timeout reached")
return
case err := <-resultCh:
require.True(t, util.IsCollectResultsError(err), "isCollectResultsError")
p.PrintResults()
}
require.Empty(t, out.String(), "output should be empty")
})
t.Run("With --print-query outputs query then all lines", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
p := newPeco()
p.Argv = []string{"--select-all", "--print-query", "--query", "test"}
p.Stdin = bytes.NewBufferString("foo\nbar\n")
var out bytes.Buffer
p.Stdout = &out
resultCh := make(chan error)
go func() {
defer close(resultCh)
select {
case <-ctx.Done():
return
case resultCh <- p.Run(ctx):
return
}
}()
select {
case <-ctx.Done():
t.Errorf("timeout reached")
return
case err := <-resultCh:
require.True(t, util.IsCollectResultsError(err), "isCollectResultsError")
p.PrintResults()
}
require.Equal(t, "test\n", out.String(), "output should have query and no matching lines")
})
t.Run("With query filters then selects all matches", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
p := newPeco()
p.Argv = []string{"--select-all", "--query", "foo"}
p.Stdin = bytes.NewBufferString("foo\nbar\nfoobar\n")
var out bytes.Buffer
p.Stdout = &out
resultCh := make(chan error)
go func() {
defer close(resultCh)
select {
case <-ctx.Done():
return
case resultCh <- p.Run(ctx):
return
}
}()
select {
case <-ctx.Done():
t.Errorf("timeout reached")
return
case err := <-resultCh:
require.True(t, util.IsCollectResultsError(err), "isCollectResultsError")
p.PrintResults()
}
require.Equal(t, "foo\nfoobar\n", out.String(), "output should contain only matching lines")
})
}
func TestPrintQuery(t *testing.T) {
t.Run("Match and print query", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)