peco.peco/cli.go
2015-03-17 15:37:53 +09:00

231 lines
5.2 KiB
Go

package peco
import (
"fmt"
"os"
"reflect"
"runtime"
"github.com/jessevdk/go-flags"
"github.com/nsf/termbox-go"
)
type CLIOptions struct {
OptHelp bool `short:"h" long:"help" description:"show this help message and exit"`
OptTTY string `long:"tty" description:"path to the TTY (usually, the value of $TTY)"`
OptQuery string `long:"query" description:"initial value for query"`
OptRcfile string `long:"rcfile" description:"path to the settings file"`
OptVersion bool `long:"version" description:"print the version and exit"`
OptBufferSize int `long:"buffer-size" short:"b" description:"number of lines to keep in search buffer"`
OptEnableNullSep bool `long:"null" description:"expect NUL (\\0) as separator for target/output"`
OptInitialIndex int `long:"initial-index" description:"position of the initial index of the selection (0 base)"`
OptInitialMatcher string `long:"initial-matcher" description:"specify the default matcher (deprecated)"`
OptInitialFilter string `long:"initial-filter" description:"specify the default filter"`
OptPrompt string `long:"prompt" description:"specify the prompt string"`
OptLayout string `long:"layout" description:"layout to be used 'top-down' (default) or 'bottom-up'" default:"top-down"`
}
func showHelp() {
// The ONLY reason we're not using go-flags' help option is
// because I wanted to tweak the format just a bit... but
// there wasn't an easy way to do so
os.Stderr.WriteString(`
Usage: peco [options] [FILE]
Options:
`)
t := reflect.TypeOf(CLIOptions{})
for i := 0; i < t.NumField(); i++ {
tag := t.Field(i).Tag
var o string
if s := tag.Get("short"); s != "" {
o = fmt.Sprintf("-%s, --%s", tag.Get("short"), tag.Get("long"))
} else {
o = fmt.Sprintf("--%s", tag.Get("long"))
}
fmt.Fprintf(
os.Stderr,
" %-21s %s\n",
o,
tag.Get("description"),
)
}
}
// BufferSize returns the specified buffer size. Fulfills CtxOptions
func (o CLIOptions) BufferSize() int {
return o.OptBufferSize
}
// EnableNullSep returns tru if --null was specified. Fulfills CtxOptions
func (o CLIOptions) EnableNullSep() bool {
return o.OptEnableNullSep
}
func (o CLIOptions) InitialIndex() int {
return o.OptInitialIndex
}
func (o CLIOptions) LayoutType() string {
return o.OptLayout
}
type CLI struct {
}
func (cli *CLI) parseOptions() (*CLIOptions, []string, error) {
opts := &CLIOptions{}
p := flags.NewParser(opts, flags.PrintErrors)
args, err := p.Parse()
if err != nil {
showHelp()
return nil, nil, err
}
if opts.OptLayout != "" {
if !IsValidLayoutType(LayoutType(opts.OptLayout)) {
return nil, nil, fmt.Errorf("unknown layout: '%s'\n", opts.OptLayout)
}
}
return opts, args, nil
}
func (cli *CLI) Run() error {
opts, args, err := cli.parseOptions()
if err != nil {
return err
}
if opts.OptHelp {
showHelp()
return nil
}
if opts.OptVersion {
fmt.Fprintf(os.Stderr, "peco: %s\n", version)
return nil
}
var in *os.File
// receive in from either a file or Stdin
switch {
case len(args) > 0:
in, err = os.Open(args[0])
if err != nil {
return err
}
case !IsTty(os.Stdin.Fd()):
in = os.Stdin
default:
return fmt.Errorf("error: You must supply something to work with via filename or stdin")
}
ctx := NewCtx(opts)
defer func() {
ch := ctx.ResultCh()
if ch == nil {
return
}
for match := range ch {
line := match.Output()
if line[len(line)-1] != '\n' {
line = line + "\n"
}
fmt.Fprint(os.Stdout, line)
}
}()
if opts.OptRcfile == "" {
file, err := LocateRcfile()
if err == nil {
opts.OptRcfile = file
}
}
// Default matcher is IgnoreCase
ctx.SetCurrentFilterByName(IgnoreCaseMatch)
if opts.OptRcfile != "" {
err = ctx.ReadConfig(opts.OptRcfile)
if err != nil {
return err
}
}
if len(opts.OptPrompt) > 0 {
ctx.SetPrompt(opts.OptPrompt)
}
initialFilter := ""
if len(opts.OptInitialFilter) <= 0 && len(opts.OptInitialMatcher) > 0 {
initialFilter = opts.OptInitialMatcher
} else if len(opts.OptInitialFilter) > 0 {
initialFilter = opts.OptInitialFilter
}
if initialFilter != "" {
if err := ctx.SetCurrentFilterByName(initialFilter); err != nil {
return fmt.Errorf("unknown matcher: '%s'\n", initialFilter)
}
}
// Try waiting for something available in the source stream
// before doing any terminal initialization (also done by termbox)
reader := ctx.NewBufferReader(in)
ctx.AddWaitGroup(1)
go reader.Loop()
// This channel blocks until we receive something from `in`
<-reader.InputReadyCh()
err = TtyReady()
if err != nil {
return err
}
defer TtyTerm()
err = termbox.Init()
if err != nil {
return err
}
defer termbox.Close()
// Windows handle Esc/Alt self
if runtime.GOOS == "windows" {
termbox.SetInputMode(termbox.InputEsc | termbox.InputAlt)
}
ctx.startInput()
view := ctx.NewView()
filter := ctx.NewFilter()
sig := ctx.NewSignalHandler()
loopers := []interface {
Loop()
}{
view,
filter,
sig,
}
for _, looper := range loopers {
ctx.AddWaitGroup(1)
go looper.Loop()
}
if len(opts.OptQuery) > 0 {
ctx.SetQuery([]rune(opts.OptQuery))
ctx.ExecQuery()
} else {
ctx.SendDraw()
}
ctx.WaitDone()
return ctx.Error()
}