Merge pull request #394 from peco/topic/command

[WIP] Remove ExecuteCommand, and add --exec
This commit is contained in:
lestrrat 2017-03-03 20:28:31 +09:00 committed by GitHub
commit 766fdd73c0
13 changed files with 217 additions and 120 deletions

15
Changes
View file

@ -1,6 +1,21 @@
Changes
=======
v0.5.0 - unreleased
Backwards Incompatible Changes:
* ExecuteCommand has been removed.
Features:
* A new command line option `--exec` has been added. This allows you to
execute external commands via shell, and should be used as replacement
to `ExecuteCommand`
* A new configuration option `MaxScanBufferSize` has been added. Whereas
bufio.Scanner (which peco internally relies on) only accepts lines that
are < 64kb when reading the input, specifying this option in the config
allows you to change this limit.
Bugs/Fixes
* When executing external commands, the screen and the capturing of user
input would interfere when getting back to peco.
v0.4.9 - 01 Mar 2017
Bugs/Fixes
* SavedSelection under `--selection-prefix` was not properly working

View file

@ -183,6 +183,18 @@ If there are multiple lines in the input, the usual selection view is displayed.
Specifies the exit status to use when the user cancels the query execution.
For historical and back-compatibility reasons, the default is `success`, meaning if the user cancels the query, the exit status is 0. When you choose `error`, peco will exit with a non-zero value.
### --selection-prefix `string`
When specified, peco uses the specified prefix instead of changing line color to indicate currently selected line(s). default is to use colors. This option is experimental
### --exec `string`
When specified, peco executes the specified external command (via shell), with peco's currently selected line(s) as its input from STDIN.
Upon exiting from the external command, the control goes back to peco where you can keep browsing your search buffer, and to possibly execute your external command repeatedly afterwards.
To exit out of peco when running in this mode, you must execute the Cancel command, usually the escape key.
# Configuration File
peco by default consults a few locations for the config files.
@ -251,6 +263,18 @@ Default value for StickySelection is false.
OnCancel is equivalent to `--on-cancel` command line option.
### MaxScanBufferSize
```json
{
"MaxScanBufferSize": 256
}
```
Controls the buffer sized (in kilobytes) used by `bufio.Scanner`, which is
responsible for reading the input lines. If you believe that your input has
very long lines that prohibit peco from reading them, try increasing this number.
## Keymaps
Example:
@ -538,23 +562,6 @@ See --layout.
}
```
## ExecuteCommand
```
{
"Keymap": {
"C-e": "peco.ExecuteCommand.Notepad"
},
"Command": [
{
"Name": "Notepad",
"Args": ["notepad", "$FILE"],
"Spawn": true
}
]
}
```
## SelectionPrefix
`SelectionPrefix` is equivalent to using `--selection-prefix` in the command line.
@ -678,6 +685,7 @@ Much code stolen from https://github.com/mattn/gof
* [--select-1](#--select-1)
* [--on-cancel `success|error`](#--on-cancel-successerror)
* [--selection-prefix `string`](#--selection-prefix-string)
* [--exec `string`](#--exec-string)
* [Configuration File](#configuration-file)
* [Global](#global)
* [Prompt](#prompt)
@ -700,7 +708,6 @@ Much code stolen from https://github.com/mattn/gof
* [Examples](#examples)
* [Layout](#layout)
* [SingleKeyJump](#singlekeyjump)
* [ExecuteCommand](#executecommand)
* [SelectionPrefix](#selectionprefix)
* [FAQ](#faq)
* [Does peco work on (msys2|cygwin)?](#does-peco-work-on-msys2cygwin)

105
action.go
View file

@ -1,10 +1,8 @@
package peco
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"unicode"
"context"
@ -13,6 +11,7 @@ import (
"github.com/lestrrat/go-pdebug"
"github.com/nsf/termbox-go"
"github.com/peco/peco/internal/keyseq"
"github.com/peco/peco/internal/util"
"github.com/peco/peco/line"
"github.com/pkg/errors"
)
@ -299,7 +298,44 @@ func doFinish(ctx context.Context, state *Peco, _ termbox.Event) {
defer g.End()
}
state.Exit(errCollectResults{})
ccarg := state.execOnFinish
if len(ccarg) == 0 {
state.Exit(errCollectResults{})
return
}
sel := NewSelection()
state.Selection().Copy(sel)
if sel.Len() == 0 {
if l, err := state.CurrentLineBuffer().LineAt(state.Location().LineNumber()); err == nil {
sel.Add(l)
}
}
var stdin bytes.Buffer
sel.Ascend(func(it btree.Item) bool {
line := it.(line.Line)
stdin.WriteString(line.Buffer())
stdin.WriteRune('\n')
return true
})
var err error
state.Hub().SendStatusMsg("Executing " + ccarg)
cmd := util.Shell(ccarg)
cmd.Stdin = &stdin
cmd.Stdout = state.Stdout
cmd.Stderr = state.Stderr
state.screen.Suspend()
err = cmd.Run()
state.screen.Resume()
state.ExecQuery()
if err != nil {
// bail out, or otherwise the user cannot know what happened
state.Exit(errors.Wrap(err, `failed to execute command`))
}
}
func doCancel(ctx context.Context, state *Peco, e termbox.Event) {
@ -729,64 +765,3 @@ func makeCombinedAction(actions ...Action) ActionFunc {
}, toplevel)
})
}
func makeCommandAction(state *Peco, cc *CommandConfig) ActionFunc {
return func(ctx context.Context, state *Peco, _ termbox.Event) {
sel := state.Selection()
if sel.Len() == 0 {
if l, err := state.CurrentLineBuffer().LineAt(state.Location().LineNumber()); err == nil {
sel.Add(l)
}
}
sel.Ascend(func(it btree.Item) bool {
line := it.(line.Line)
var f *os.File
var err error
args := append([]string{}, cc.Args...)
for i, v := range args {
switch v {
case "$FILE":
if f == nil {
f, err = ioutil.TempFile("", "peco")
if err != nil {
return false
}
f.WriteString(line.Buffer())
f.Close()
}
args[i] = f.Name()
case "$LINE":
args[i] = line.Buffer()
}
}
state.Hub().SendStatusMsg("Executing " + cc.Name)
cmd := exec.Command(args[0], args[1:]...)
if cc.Spawn {
err = cmd.Start()
go func() {
cmd.Wait()
if f != nil {
os.Remove(f.Name())
}
}()
} else {
cmd.Stdin = state.Stdin
cmd.Stdout = state.Stdout
cmd.Stderr = state.Stderr
err = cmd.Run()
if f != nil {
os.Remove(f.Name())
}
state.ExecQuery()
}
if err != nil {
return false
}
return true
})
}
}

View file

@ -5,9 +5,11 @@ import (
"os"
"runtime"
"context"
pdebug "github.com/lestrrat/go-pdebug"
"github.com/peco/peco"
"github.com/peco/peco/internal/util"
"context"
)
func main() {
@ -21,6 +23,9 @@ func main() {
}
func _main() int {
if pdebug.Enabled {
pdebug.DefaultCtx.Writer = os.Stderr
}
if envvar := os.Getenv("GOMAXPROCS"); envvar == "" {
runtime.GOMAXPROCS(runtime.NumCPU())
}
@ -42,7 +47,6 @@ func _main() int {
return 1
}
}
return 0

View file

@ -70,6 +70,7 @@ type Peco struct {
config Config
currentLineBuffer Buffer
enableSep bool // Enable parsing on separators
execOnFinish string
filters filter.Set
idgen *idgen
initialFilter string
@ -78,6 +79,7 @@ type Peco struct {
keymap Keymap
layoutType string
location Location
maxScanBufferSize int
mutex sync.Mutex
onCancel string
prompt string
@ -156,16 +158,20 @@ type Screen interface {
Init() error
Close() error
Flush() error
PollEvent() chan termbox.Event
PollEvent(context.Context) chan termbox.Event
Print(PrintArgs) int
Resume()
SetCell(int, int, rune, termbox.Attribute, termbox.Attribute)
Size() (int, int)
SendEvent(termbox.Event)
Suspend()
}
// Termbox just hands out the processing to the termbox library
type Termbox struct {
mutex sync.Mutex
mutex sync.Mutex
resumeCh chan (struct{})
suspendCh chan (struct{})
}
// View handles the drawing/updating the screen
@ -292,9 +298,9 @@ type Config struct {
OnCancel string `json:"OnCancel"`
CustomMatcher map[string][]string
CustomFilter map[string]CustomFilterConfig
Command []CommandConfig
QueryExecutionDelay int
StickySelection bool
MaxScanBufferSize int
// If this is true, then the prefix for single key jump mode
// is displayed by default.
@ -308,17 +314,6 @@ type SingleKeyJumpConfig struct {
ShowPrefix bool `json:"ShowPrefix"`
}
type CommandConfig struct {
// Name is the name of the command to execute
Name string
// TODO: need to check if how we use this is correct
Args []string
// Spawn mean the command should be executed asynchronous.
Spawn bool
}
// CustomFilterConfig is used to specify configuration parameters
// to CustomFilters
type CustomFilterConfig struct {
@ -421,6 +416,7 @@ type CLIOptions struct {
OptSelect1 bool `long:"select-1" description:"select first item and immediately exit if the input contains only 1 item"`
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'"`
}
type CLI struct {

View file

@ -0,0 +1,18 @@
// +build !windows
package util
import "os/exec"
func Shell(cmd ...string) *exec.Cmd {
const shellpath = `/bin/sh`
const shellopt = `-c`
args := make([]string, len(cmd) + 1)
args[0] = shellopt
for i := 0; i < len(cmd); i++ {
args[i+1] = cmd[i]
}
return exec.Command(shellpath, args...)
}

View file

@ -0,0 +1,18 @@
// +build windows
package util
import "os/exec"
func Shell(cmd ...string) *exec.Cmd {
const shellpath = `cmd`
const shellopt = `/c`
args := make([]string, len(cmd) + 1)
args[0] = shellopt
for i := 0; i < len(cmd); i++ {
args[i+1] = cmd[i]
}
return exec.Command(shellpath, args...)
}

30
peco.go
View file

@ -1,6 +1,7 @@
package peco
import (
"bufio"
"bytes"
"io"
"os"
@ -113,8 +114,9 @@ func New() *Peco {
idgen: newIDGen(),
queryExecDelay: 50 * time.Millisecond,
readyCh: make(chan struct{}),
screen: &Termbox{},
screen: NewTermbox(),
selection: NewSelection(),
maxScanBufferSize: bufio.MaxScanTokenSize,
}
}
@ -326,7 +328,7 @@ func (p *Peco) Run(ctx context.Context) (err error) {
loopers := []interface {
Loop(ctx context.Context, cancel func()) error
}{
NewInput(p, p.Keymap(), p.screen.PollEvent()),
NewInput(p, p.Keymap(), p.screen.PollEvent(ctx)),
NewView(p),
NewFilter(p),
sig.New(sig.SigReceivedHandlerFunc(func(sig os.Signal) {
@ -482,17 +484,6 @@ func readConfig(cfg *Config, filename string) error {
return nil
}
func (p *Peco) populateCommandList() error {
for _, v := range p.config.Command {
if len(v.Args) == 0 {
continue
}
makeCommandAction(p, &v).Register("ExecuteCommand." + v.Name)
}
return nil
}
func (p *Peco) ApplyConfig(opts CLIOptions) error {
// If layoutType is not set and is set in the config, set it
if p.layoutType == "" {
@ -503,6 +494,15 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error {
}
}
p.maxScanBufferSize = 256
if v := p.config.MaxScanBufferSize; v > 0 {
p.maxScanBufferSize = v
}
if v := opts.OptExec; len(v) > 0 {
p.execOnFinish = v
}
p.enableSep = opts.OptEnableNullSep
if i := opts.OptInitialIndex; i >= 0 {
@ -540,10 +540,6 @@ func (p *Peco) ApplyConfig(opts CLIOptions) error {
p.initialFilter = opts.OptInitialMatcher
}
if err := p.populateCommandList(); err != nil {
return errors.Wrap(err, "failed to populate command list")
}
if err := p.populateFilters(); err != nil {
return errors.Wrap(err, "failed to populate filters")
}

View file

@ -124,12 +124,14 @@ func (d dummyScreen) Flush() error {
d.record("Flush", interceptorArgs{})
return nil
}
func (d dummyScreen) PollEvent() chan termbox.Event {
func (d dummyScreen) PollEvent(ctx context.Context) chan termbox.Event {
return d.pollCh
}
func (d dummyScreen) Size() (int, int) {
return d.width, d.height
}
func (d dummyScreen) Resume() {}
func (d dummyScreen) Suspend() {}
func TestIDGen(t *testing.T) {
idgen := newIDGen()

View file

@ -1,8 +1,10 @@
package peco
import (
"context"
"unicode/utf8"
pdebug "github.com/lestrrat/go-pdebug"
"github.com/mattn/go-runewidth"
"github.com/nsf/termbox-go"
"github.com/pkg/errors"
@ -16,6 +18,13 @@ func (t *Termbox) Init() error {
return t.PostInit()
}
func NewTermbox() *Termbox {
return &Termbox{
suspendCh: make(chan struct{}),
resumeCh: make(chan struct{}),
}
}
func (t *Termbox) Close() error {
termbox.Close()
return nil
@ -38,7 +47,7 @@ func (t *Termbox) Flush() error {
// PollEvent returns a channel that you can listen to for
// termbox's events. The actual polling is done in a
// separate gouroutine
func (t *Termbox) PollEvent() chan termbox.Event {
func (t *Termbox) PollEvent(ctx context.Context) chan termbox.Event {
// XXX termbox.PollEvent() can get stuck on unexpected signal
// handling cases. We still would like to wait until the user
// (termbox) has some event for us to process, but we don't
@ -49,17 +58,60 @@ func (t *Termbox) PollEvent() chan termbox.Event {
// safely be implemented in terms of select {} which is
// safe from being stuck.
evCh := make(chan termbox.Event)
go func() {
// keep listening to suspend requests here
for {
select {
case <-ctx.Done():
return
case <-t.suspendCh:
if pdebug.Enabled {
pdebug.Printf("poll event suspended!")
}
termbox.Interrupt()
t.Close()
}
}
}()
go func() {
defer func() { recover() }()
defer func() { close(evCh) }()
for {
evCh <- termbox.PollEvent()
ev := termbox.PollEvent()
if ev.Type != termbox.EventInterrupt {
evCh <- ev
continue
}
select {
case <-ctx.Done():
return
case <-t.resumeCh:
t.Init()
}
}
}()
return evCh
}
func (t *Termbox) Suspend() {
select {
case t.suspendCh <- struct{}{}:
default:
}
}
func (t *Termbox) Resume() {
select {
case t.resumeCh <- struct{}{}:
default:
}
}
// SetCell writes to the terminal
func (t *Termbox) SetCell(x, y int, ch rune, fg, bg termbox.Attribute) {
t.mutex.Lock()

View file

@ -20,6 +20,13 @@ func (s *Selection) Add(l line.Line) {
s.tree.ReplaceOrInsert(l)
}
func (s *Selection) Copy(dst *Selection) {
s.Ascend(func(it btree.Item) bool {
dst.Add(it.(line.Line))
return true
})
}
// Remove removes the specified line from the selection
func (s *Selection) Remove(l line.Line) {
s.mutex.Lock()

View file

@ -77,7 +77,12 @@ func (s *Source) Setup(ctx context.Context, state *Peco) {
// Note: this will be a no-op if notify.Do has been called before
defer notify.Do(notifycb)
if pdebug.Enabled {
pdebug.Printf("Source: using buffer size of %dkb", state.maxScanBufferSize)
}
scanbuf := make([]byte, state.maxScanBufferSize*1024)
scanner := bufio.NewScanner(s.in)
scanner.Buffer(scanbuf, state.maxScanBufferSize*1024)
defer func() {
if util.IsTty(s.in) {
return

View file

@ -44,7 +44,9 @@ func TestSource(t *testing.T) {
r := addReadDelay(strings.NewReader(strings.Join(lines, "\n")), 2*time.Second)
s := NewSource(r, ig, 0, false)
go s.Setup(ctx, &Peco{hub: nullHub{}})
p := New()
p.hub = nullHub{}
go s.Setup(ctx, p)
timeout := time.After(5 * time.Second)
waitout := time.After(1 * time.Second)