├── .gitignore ├── README.md ├── _example ├── main.go └── select.go ├── finder.go ├── fzf.go ├── fzy.go ├── go.mod ├── go.sum ├── installer └── installer.go ├── peco.go └── source └── source.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-finder 2 | ========= 3 | 4 | [![GoDoc](https://godoc.org/github.com/b4b4r07/go-finder?status.svg)](https://godoc.org/github.com/b4b4r07/go-finder) 5 | 6 | CLI finder wrapper (fzf, peco, etc) for golang 7 | 8 | ## Usage 9 | 10 | ```go 11 | fzf, err := finder.New("fzf", "--reverse", "--height", "40") 12 | if err != nil { 13 | panic(err) 14 | } 15 | fzf.Run() 16 | ``` 17 | 18 | ```go 19 | peco, err := finder.New("peco") 20 | if err != nil { 21 | panic(err) 22 | } 23 | peco.Run() 24 | ``` 25 | 26 | ```go 27 | cli, err := finder.New() 28 | if err != nil { 29 | panic(err) 30 | } 31 | // If no argument is given to finder.New() 32 | // it scans available finder command (fzf,fzy,peco,etc) from your PATH 33 | ``` 34 | 35 | ## Installation 36 | 37 | ```console 38 | $ go get -d github.com/b4b4r07/go-finder 39 | ``` 40 | 41 | ## License 42 | 43 | MIT 44 | 45 | ## Author 46 | 47 | b4b4r07 48 | -------------------------------------------------------------------------------- /_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | finder "github.com/b4b4r07/go-finder" 7 | "github.com/b4b4r07/go-finder/source" 8 | ) 9 | 10 | func main() { 11 | fzf, err := finder.New("fzf") 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | // If needed, install fzf to the path 17 | fzf.Install("/usr/local/bin") 18 | 19 | fmt.Printf("fzf obeject: %#v\n", fzf) 20 | 21 | // Read files list within dir as data source of fzf 22 | fzf.Read(source.Dir(".", true)) 23 | 24 | items, err := fzf.Run() 25 | if err != nil { 26 | panic(err) 27 | } 28 | fmt.Printf("selected items:%#v\n", items) 29 | } 30 | -------------------------------------------------------------------------------- /_example/select.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | finder "github.com/b4b4r07/go-finder" 7 | ) 8 | 9 | func main() { 10 | fzf, err := finder.New("fzf") 11 | if err != nil { 12 | panic(err) 13 | } 14 | 15 | type Book struct { 16 | Title string 17 | ISBN string 18 | } 19 | 20 | books := []Book{ 21 | Book{ 22 | Title: "Book A", 23 | ISBN: "aaa", 24 | }, 25 | Book{ 26 | Title: "Book B", 27 | ISBN: "bbb", 28 | }, 29 | Book{ 30 | Title: "Book C", 31 | ISBN: "ccc", 32 | }, 33 | } 34 | items := finder.NewItems() 35 | for _, book := range books { 36 | items.Add(book.Title, book) 37 | } 38 | selectedItems, err := fzf.Select(items) 39 | if err != nil { 40 | panic(err) 41 | } 42 | for _, item := range selectedItems { 43 | fmt.Printf("ISBN of %s is %s\n", item.(Book).Title, item.(Book).ISBN) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /finder.go: -------------------------------------------------------------------------------- 1 | package finder 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strings" 7 | 8 | "github.com/b4b4r07/go-finder/source" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // CLI is the command having a command-line interface 13 | type CLI interface { 14 | Run() ([]string, error) 15 | Read(source.Source) 16 | } 17 | 18 | // Item is key-value 19 | type Item struct { 20 | Key string 21 | Value interface{} 22 | } 23 | 24 | // Items is the collection of Item 25 | type Items []Item 26 | 27 | // NewItems creates Items object 28 | func NewItems() Items { 29 | return Items{} 30 | } 31 | 32 | // Add addes item to Items 33 | func (i *Items) Add(key string, value interface{}) { 34 | *i = append(*i, Item{ 35 | Key: key, 36 | Value: value, 37 | }) 38 | } 39 | 40 | // Finder is the interface of a filter command 41 | type Finder interface { 42 | CLI 43 | Install(string) error 44 | Select(interface{}) ([]interface{}, error) 45 | } 46 | 47 | // Command represents the command 48 | type Command struct { 49 | Name string 50 | Args []string 51 | Path string 52 | Items Items 53 | Source source.Source 54 | } 55 | 56 | // Commands represents the command list 57 | type Commands []Command 58 | 59 | // DefaultCommands represents the list of default finder commands optimized for quick usage 60 | var DefaultCommands = Commands{ 61 | // https://github.com/junegunn/fzf 62 | Command{ 63 | Name: "fzf", 64 | Args: []string{"--reverse", "--height=50%", "--ansi", "--multi"}, 65 | }, 66 | // https://github.com/jhawthorn/fzy 67 | Command{Name: "fzy"}, 68 | // https://github.com/peco/peco 69 | Command{Name: "peco"}, 70 | // https://github.com/mooz/percol 71 | Command{Name: "percol"}, 72 | } 73 | 74 | // Lookup lookups the available command 75 | func (c Commands) Lookup() (Command, error) { 76 | for _, command := range c { 77 | path, err := exec.LookPath(command.Name) 78 | if err == nil { 79 | return Command{ 80 | Name: command.Name, 81 | Args: command.Args, 82 | Path: path, 83 | Source: source.Stdin(), 84 | }, nil 85 | } 86 | } 87 | return Command{}, errors.New("no available finder command") 88 | } 89 | 90 | // Run runs as a command 91 | func (c *Command) Run() ([]string, error) { 92 | shell := os.Getenv("SHELL") 93 | if len(shell) == 0 { 94 | shell = "sh" 95 | } 96 | cmd := exec.Command(shell, "-c", c.Path+" "+strings.Join(c.Args, " ")) 97 | cmd.Stderr = os.Stderr 98 | in, _ := cmd.StdinPipe() 99 | errCh := make(chan error, 1) 100 | go func() { 101 | if err := c.Source(in); err != nil { 102 | errCh <- err 103 | return 104 | } 105 | errCh <- nil 106 | in.Close() 107 | }() 108 | err := <-errCh 109 | if err != nil { 110 | return []string{}, err 111 | } 112 | result, _ := cmd.Output() 113 | return trimLastNewline(strings.Split(string(result), "\n")), nil 114 | } 115 | 116 | // Select selects the keys in various map 117 | func (c *Command) Select(args interface{}) ([]interface{}, error) { 118 | switch items := args.(type) { 119 | case Items: 120 | var keys []string 121 | for _, item := range items { 122 | keys = append(keys, item.Key) 123 | } 124 | if len(keys) == 0 { 125 | return nil, errors.New("no items") 126 | } 127 | c.Read(source.Slice(keys)) 128 | selectedKeys, err := c.Run() 129 | if err != nil { 130 | return nil, err 131 | } 132 | var values []interface{} 133 | for _, key := range selectedKeys { 134 | for _, item := range items { 135 | if item.Key == key { 136 | values = append(values, item.Value) 137 | } 138 | } 139 | } 140 | return values, nil 141 | case []string: 142 | if len(items) == 0 { 143 | return nil, errors.New("no items") 144 | } 145 | c.Read(source.Slice(items)) 146 | selectedItems, err := c.Run() 147 | if err != nil { 148 | return nil, err 149 | } 150 | var values []interface{} 151 | for _, item := range selectedItems { 152 | values = append(values, item) 153 | } 154 | return values, nil 155 | default: 156 | return nil, errors.New("Error") 157 | } 158 | } 159 | 160 | func trimLastNewline(s []string) []string { 161 | if len(s) == 0 { 162 | return s 163 | } 164 | last := len(s) - 1 165 | if s[last] == "" { 166 | return s[:last] 167 | } 168 | return s 169 | } 170 | 171 | // Install does nothing and is implemented to satisfy Finder interface 172 | // This method should be overwritten by each finder command implementation 173 | func (c *Command) Install(path string) error { 174 | return nil 175 | } 176 | 177 | // Read sets the data sources 178 | func (c *Command) Read(data source.Source) { 179 | c.Source = data 180 | } 181 | 182 | // New creates Finder instance 183 | func New(args ...string) (Finder, error) { 184 | var ( 185 | command Command 186 | err error 187 | ) 188 | if len(args) == 0 { 189 | command, err = DefaultCommands.Lookup() 190 | if err != nil { 191 | return nil, err 192 | } 193 | } else { 194 | path, err := exec.LookPath(args[0]) 195 | if err != nil { 196 | return nil, errors.Wrapf(err, "%s: not found", args[0]) 197 | } 198 | command = Command{ 199 | Name: args[0], 200 | Args: args[1:], 201 | Path: path, 202 | Items: Items{}, 203 | Source: source.Stdin(), 204 | } 205 | } 206 | switch command.Name { 207 | case "fzf": 208 | return Fzf{&command}, nil 209 | case "fzy": 210 | return Fzy{&command}, nil 211 | case "peco": 212 | return Peco{&command}, nil 213 | default: 214 | return &command, nil 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /fzf.go: -------------------------------------------------------------------------------- 1 | package finder 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | 9 | "github.com/b4b4r07/go-finder/installer" 10 | ) 11 | 12 | // Fzf represents the filter instance 13 | type Fzf struct { 14 | *Command 15 | } 16 | 17 | // Install installs the command 18 | func (c Fzf) Install(path string) error { 19 | bin := filepath.Join(path, "fzf") 20 | if _, err := os.Stat(bin); err == nil { 21 | // Already installed, no need to install 22 | return nil 23 | } 24 | 25 | release := installer.GitHubRelease{ 26 | Owner: "junegunn", 27 | Repo: "fzf-bin", 28 | Version: "0.17.4", 29 | Package: fmt.Sprintf("fzf-%s-%s_%s.tgz", "0.17.4", runtime.GOOS, runtime.GOARCH), 30 | } 31 | 32 | tarball, err := release.Grab() 33 | if err != nil { 34 | return err 35 | } 36 | 37 | pkg := installer.New(tarball) 38 | if err := pkg.Unpack(); err != nil { 39 | return err 40 | } 41 | 42 | err = pkg.Install(path) 43 | if err == nil { 44 | c.Path = bin 45 | } 46 | 47 | return err 48 | } 49 | -------------------------------------------------------------------------------- /fzy.go: -------------------------------------------------------------------------------- 1 | package finder 2 | 3 | // Fzy represents the filter instance 4 | type Fzy struct { 5 | *Command 6 | } 7 | 8 | // Install installs the command 9 | func (c Fzy) Install(path string) error { 10 | // not support yet 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/b4b4r07/go-finder 2 | 3 | go 1.12 4 | 5 | require github.com/pkg/errors v0.8.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 2 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 3 | -------------------------------------------------------------------------------- /installer/installer.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | ) 13 | 14 | // Package represents the archive package 15 | type Package struct { 16 | Archive string 17 | Binary string 18 | Permission int64 19 | } 20 | 21 | // New create the package 22 | func New(archive string) Package { 23 | return Package{ 24 | Archive: archive, 25 | } 26 | } 27 | 28 | // Unpack unpacks the archive package 29 | func (p *Package) Unpack() error { 30 | file, err := os.Open(p.Archive) 31 | if err != nil { 32 | return err 33 | } 34 | defer file.Close() 35 | 36 | var fileReader io.ReadCloser = file 37 | if fileReader, err = gzip.NewReader(file); err != nil { 38 | return err 39 | } 40 | defer fileReader.Close() 41 | 42 | defer os.Remove(p.Archive) 43 | 44 | tarBallReader := tar.NewReader(fileReader) 45 | for { 46 | header, err := tarBallReader.Next() 47 | if err != nil { 48 | if err == io.EOF { 49 | break 50 | } 51 | return err 52 | } 53 | 54 | // get the individual filename and extract to the current directory 55 | filename := header.Name 56 | 57 | p.Binary = header.Name 58 | p.Permission = header.Mode 59 | 60 | switch header.Typeflag { 61 | case tar.TypeDir: 62 | err = os.MkdirAll(filename, os.FileMode(header.Mode)) 63 | if err != nil { 64 | return err 65 | } 66 | case tar.TypeReg: 67 | writer, err := os.Create(filename) 68 | if err != nil { 69 | return err 70 | } 71 | io.Copy(writer, tarBallReader) 72 | err = os.Chmod(filename, os.FileMode(header.Mode)) 73 | if err != nil { 74 | return err 75 | } 76 | writer.Close() 77 | default: 78 | return fmt.Errorf("Unable to untar type: %c in file %s", header.Typeflag, filename) 79 | } 80 | } 81 | return nil 82 | } 83 | 84 | // Install installs the file to PATH 85 | func (p *Package) Install(path string) error { 86 | if path == "" { 87 | return fmt.Errorf("%s: invalid path", path) 88 | } 89 | file := filepath.Join(path, p.Binary) 90 | err := copy(p.Binary, file) 91 | if err != nil { 92 | return err 93 | } 94 | defer os.Remove(p.Binary) 95 | return os.Chmod(file, os.FileMode(p.Permission)) 96 | } 97 | 98 | func copy(srcName, dstName string) error { 99 | src, err := os.Open(srcName) 100 | if err != nil { 101 | return err 102 | } 103 | defer src.Close() 104 | 105 | dst, err := os.Create(dstName) 106 | if err != nil { 107 | return err 108 | } 109 | defer dst.Close() 110 | 111 | _, err = io.Copy(dst, src) 112 | return err 113 | } 114 | 115 | // GitHubRelease represents the GitHub Releases 116 | type GitHubRelease struct { 117 | Owner string 118 | Repo string 119 | Version string 120 | Package string 121 | } 122 | 123 | // Grab grabs the release binary from GitHub Releases 124 | func (r GitHubRelease) Grab() (string, error) { 125 | resp, err := http.Get(fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s", 126 | r.Owner, r.Repo, r.Version, r.Package, 127 | )) 128 | if err != nil { 129 | return r.Package, err 130 | } 131 | defer resp.Body.Close() 132 | 133 | file, err := os.Create(r.Package) 134 | if err != nil { 135 | return r.Package, err 136 | } 137 | defer file.Close() 138 | 139 | body, err := ioutil.ReadAll(resp.Body) 140 | if err != nil { 141 | return r.Package, err 142 | } 143 | file.Write(body) 144 | 145 | return r.Package, nil 146 | } 147 | -------------------------------------------------------------------------------- /peco.go: -------------------------------------------------------------------------------- 1 | package finder 2 | 3 | // Peco represents the filter instance 4 | type Peco struct { 5 | *Command 6 | } 7 | 8 | // Install installs the command 9 | func (c Peco) Install(path string) error { 10 | // not support yet 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /source/source.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "runtime" 12 | ) 13 | 14 | // Source is the data source for filter command 15 | type Source func(io.WriteCloser) error 16 | 17 | // Text shows the string text splitted by newlines 18 | func Text(text string) Source { 19 | return func(out io.WriteCloser) error { 20 | fmt.Fprintln(out, text) 21 | return nil 22 | } 23 | } 24 | 25 | // Dir scans the directories passed as arguments to enumerate items 26 | func Dir(dir string, full bool) Source { 27 | return func(out io.WriteCloser) error { 28 | files, err := ioutil.ReadDir(dir) 29 | if err != nil { 30 | return err 31 | } 32 | for _, file := range files { 33 | fname := file.Name() 34 | if full { 35 | fname = filepath.Join(dir, fname) 36 | } 37 | fmt.Fprintln(out, fname) 38 | } 39 | return nil 40 | } 41 | } 42 | 43 | // Reader is the source of io.Reader 44 | func Reader(r io.Reader) Source { 45 | return func(out io.WriteCloser) error { 46 | scanner := bufio.NewScanner(r) 47 | for scanner.Scan() { 48 | fmt.Fprintln(out, scanner.Text()) 49 | } 50 | return scanner.Err() 51 | } 52 | } 53 | 54 | // Stdin reads the contents from os.Stdin 55 | func Stdin() Source { 56 | return Reader(os.Stdin) 57 | } 58 | 59 | // File shows the file contents splitted by newlines 60 | func File(file string) Source { 61 | return func(out io.WriteCloser) error { 62 | fp, err := os.Open(file) 63 | if err != nil { 64 | return err 65 | } 66 | defer fp.Close() 67 | scanner := bufio.NewScanner(fp) 68 | for scanner.Scan() { 69 | fmt.Fprintln(out, scanner.Text()) 70 | } 71 | return scanner.Err() 72 | } 73 | } 74 | 75 | // Command reads the execution result of the external command as data source 76 | func Command(command string, args ...string) Source { 77 | return func(out io.WriteCloser) error { 78 | if _, err := exec.LookPath(command); err != nil { 79 | return err 80 | } 81 | for _, arg := range args { 82 | command += " " + arg 83 | } 84 | var cmd *exec.Cmd 85 | if runtime.GOOS == "windows" { 86 | cmd = exec.Command("cmd", "/c", command) 87 | } else { 88 | cmd = exec.Command("sh", "-c", command) 89 | } 90 | cmd.Stderr = os.Stderr 91 | cmd.Stdout = out 92 | cmd.Stdin = os.Stdin 93 | return cmd.Run() 94 | } 95 | } 96 | 97 | // Slice reads the string array as data source 98 | func Slice(s []string) Source { 99 | return func(out io.WriteCloser) error { 100 | for _, item := range s { 101 | fmt.Fprintln(out, item) 102 | } 103 | return nil 104 | } 105 | } 106 | --------------------------------------------------------------------------------