This commit is contained in:
Daisuke Maki 2026-02-15 09:11:18 +09:00
parent 4ed16af465
commit b560fcbbe7
3 changed files with 107 additions and 0 deletions

View file

@ -96,6 +96,7 @@ type Peco struct {
selection *Selection
selectionPrefix string
selectionRangeStart RangeStart
exitZeroAndExit bool // True if --exit-0 is enabled
selectOneAndExit bool // True if --select-1 is enabled
singleKeyJumpMode bool
singleKeyJumpPrefixes []rune
@ -442,6 +443,7 @@ type CLIOptions struct {
OptPrompt string `long:"prompt" description:"specify the prompt string"`
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"`
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'"`

15
peco.go
View file

@ -313,6 +313,12 @@ func (p *Peco) selectOneAndExitIfPossible() {
}
}
func (p *Peco) exitZeroIfPossible() {
if p.CurrentLineBuffer().Size() == 0 {
p.Exit(setExitStatus(makeIgnorable(errors.New("no input, exiting")), 1))
}
}
func (p *Peco) Run(ctx context.Context) (err error) {
if pdebug.Enabled {
g := pdebug.Marker("Peco.Run").BindError(&err)
@ -395,6 +401,14 @@ func (p *Peco) Run(ctx context.Context) (err error) {
}()
}
// If this is enabled, exit immediately with status 1 when input is empty
if p.exitZeroAndExit {
go func() {
<-p.source.SetupDone()
p.exitZeroIfPossible()
}()
}
readyOnce.Do(func() { close(p.readyCh) })
// This has tobe AFTER close(p.readyCh), otherwise the query is
@ -561,6 +575,7 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error {
p.selectionPrefix = p.config.SelectionPrefix
}
p.selectOneAndExit = opts.OptSelect1
p.exitZeroAndExit = opts.OptExitZero
p.printQuery = opts.OptPrintQuery
p.initialQuery = opts.OptQuery
p.initialFilter = opts.OptInitialFilter

View file

@ -333,6 +333,7 @@ func TestApplyConfig(t *testing.T) {
opts.OptInitialFilter = "Regexp"
opts.OptLayout = "bottom-up"
opts.OptSelect1 = true
opts.OptExitZero = true
opts.OptOnCancel = "error"
opts.OptSelectionPrefix = ">"
opts.OptPrintQuery = true
@ -374,6 +375,10 @@ func TestApplyConfig(t *testing.T) {
return
}
if !assert.Equal(t, opts.OptExitZero, p.exitZeroAndExit, "p.exitZeroAndExit should be equal to opts.OptExitZero") {
return
}
if !assert.Equal(t, opts.OptOnCancel, p.onCancel, "p.onCancel should be equal to opts.OptOnCancel") {
return
}
@ -504,6 +509,91 @@ func TestGHIssue367(t *testing.T) {
}
}
func TestExitZero(t *testing.T) {
t.Run("Empty input exits with status 1", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
p := newPeco()
p.Argv = []string{"--exit-0"}
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:
if !assert.True(t, util.IsIgnorableError(err), "error should be ignorable") {
return
}
st, ok := util.GetExitStatus(err)
if !assert.True(t, ok, "error should have exit status") {
return
}
if !assert.Equal(t, 1, st, "exit status should be 1") {
return
}
}
if !assert.Empty(t, out.String(), "output should be empty") {
return
}
})
t.Run("Non-empty input does not auto-exit", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
p := newPeco()
p.Argv = []string{"--exit-0"}
p.Stdin = bytes.NewBufferString("foo\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
}
}()
// Wait for peco to be ready, then cancel after a short delay
// If --exit-0 incorrectly triggered, we'd get an ignorable error
<-p.Ready()
time.AfterFunc(500*time.Millisecond, cancel)
select {
case <-ctx.Done():
// Expected: peco stayed running until we cancelled
case err := <-resultCh:
// If we got a result, it should NOT be an ignorable error with exit status 1
if util.IsIgnorableError(err) {
st, ok := util.GetExitStatus(err)
if ok && st == 1 {
t.Errorf("--exit-0 should not trigger when input is non-empty")
}
}
}
})
}
func TestPrintQuery(t *testing.T) {
t.Run("Match and print query", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)