Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
hay-kot committed Apr 21, 2022
0 parents commit 0e8e6ea
Show file tree
Hide file tree
Showing 11 changed files with 517 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Gofind

GoFind is a tiny and poorly written tool that I use to quickly search through my file system to quickly navigate between repositories or any directory that has a specific child. It uses the `filepath.Match` function to look for matching children in root directories, and if the child is found, it will stop return the parent directory and stop recursively looking into that parents children.

Once a match is found it's then piped to `fzf` to allow the user to select a directory and then it's spit out to the terminal which can then be used to change into that director or open it in vscode or whatever. **IMPORTANT** fzf must be installed and in the path for this to work, additionally the way I'm piping the output to fzf is probably not good practice and will likely not work on other machines YMMV.

## Config
GoFind uses a json file in `~/.config/gofind.json` to store the configuration for the search entries and the default search. It also uses this file to cache results so that the search is faster on subsequent runs. The config file example here has two jobs,

**repos:** which will recursively search the ~/Code directory for any directory that matches `.git`
**compose:** which will recursively search the ~/Docker directory for any directory that matches `docker-compose*`

I use these to either quickly find a repository I forgot the name of or where it exists, or quickly find a docker stack location and navigate to it.

```json
{
"default": "repos",
"commands": {
"compose": {
"root": "~/Docker",
"match": "docker-compose*"
},
"repos": {
"root": "~/Code",
"match": ".git"
}
},
"cache": {}
}
```

## Help

```shell
NAME:
gofind - an interactive search for directories using the filepath.Match function

USAGE:
gofind [config-entry string] e.g. `gofind repos`

COMMANDS:
cache, c cache all config entries
help, h Shows a list of commands or help for one command

GLOBAL OPTIONS:
--help, -h show help (default: false)
```
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/hay-kot/gofind

go 1.18

require github.com/urfave/cli/v2 v2.4.0

require (
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
)
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I=
github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
129 changes: 129 additions & 0 deletions gofind/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package gofind

import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
)

func ParsePath(p string) string {
// Check if path starts with ~ if it does, replace it with the user's home directory
if strings.HasPrefix(p, "~") {
homedir := Must(os.UserHomeDir())
p = filepath.Join(homedir, strings.TrimPrefix(p, "~"))
}

return p
}

type App struct {
verbose bool
}

func (a *App) LogCritical(args ...any) {
fmt.Println(args...)
}

func (a *App) LogVerbose(args ...any) {
if a.verbose {
fmt.Println(args...)
}
}

func (a *App) CacheAll() error {
config := ReadDefaultConfig()
config.CacheAll()

return nil
}

func (a *App) Run(entry string) string {
a.verbose = false

if a.verbose {
var startTime = time.Now()
defer func() {
fmt.Println("Execution time:", time.Since(startTime))
}()
}

config := ReadDefaultConfig()
// Parse Args

useDefault := false

if entry == "" {
a.LogVerbose("No arguments provided using default argument")
useDefault = true
}

cmd := config.Default

if !useDefault {
cmd = entry
}

search := config.Commands[cmd]

// Check if cache is expired
if config.Cache[cmd].IsExpired() {
a.LogVerbose("Cache is expired, searching for results and rebuilding cache")
config.Cache[cmd] = CacheEntry{
Matches: search.Results(),
Expires: time.Now().Add(time.Hour * 12),
}
config.Save()
}

matches := config.Cache[cmd].Matches

a.LogVerbose(fmt.Sprintf("Found %d matches", len(matches)))

filter := FzfFilter{}
result := filter.Find(matches)

return result.Path
}

type Match struct {
Name string
Path string
}

type SearchEntry struct {
Root string `json:"root"`
MatchStr string `json:"match"`
}

func (se SearchEntry) Results() []Match {
return SearchFor(se)
}

func SearchFor(search SearchEntry) []Match {
var matches []Match
var p = ParsePath(search.Root)

var results = Must(Finder(p, search.MatchStr))

if len(results) == 0 {
panic("No results found")
}

for _, result := range results {
match := filepath.Dir(result)
name := filepath.Base(match)

if name == "" {
continue
}

matches = append(matches, Match{
Name: name,
Path: match,
})
}

return matches
}
12 changes: 12 additions & 0 deletions gofind/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package gofind

import "time"

type CacheEntry struct {
Matches []Match `json:"matches"`
Expires time.Time `json:"expires"`
}

func (ce CacheEntry) IsExpired() bool {
return time.Now().After(ce.Expires)
}
67 changes: 67 additions & 0 deletions gofind/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package gofind

import (
"encoding/json"
"os"
"path/filepath"
"time"
)

type Config struct {
Default string `json:"default"`
Commands map[string]SearchEntry `json:"commands"`
Cache map[string]CacheEntry `json:"cache"`
}

func DefaultConfigPath() string {
homedir := Must(os.UserHomeDir())
configPath := filepath.Join(homedir, ".config", "gofind.json")

return configPath
}

func ReadConfig(path string) Config {
config := Config{}

file := Must(os.Open(path))

decoder := json.NewDecoder(file)

MustNotErr(decoder.Decode(&config))

return config
}

func ReadDefaultConfig() Config {
configPath := DefaultConfigPath()

// Check if file exists
if _, err := os.Stat(configPath); os.IsNotExist(err) {
panic("Config file does not exist")
}

return ReadConfig(configPath)
}

func (c Config) CacheAll() {
for key, search := range c.Commands {
cache := search.Results()

c.Cache[key] = CacheEntry{
Matches: cache,
Expires: time.Now().Add(time.Hour * 12),
}
}

c.Save()
}

func (c Config) Save() {
homedir := Must(os.UserHomeDir())
configPath := filepath.Join(homedir, ".config", "gofind.json")

file := Must(os.Create(configPath))
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
MustNotErr(encoder.Encode(c))
}
14 changes: 14 additions & 0 deletions gofind/err_handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package gofind

func Must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}

func MustNotErr(err error) {
if err != nil {
panic(err)
}
}
71 changes: 71 additions & 0 deletions gofind/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package gofind

import (
"fmt"
"log"
"os"
"os/exec"
"strings"
)

type InteractiveFilter interface {
Find(match []Match) Match
}

func (FzfFilter) formatSearch(repos []Match) string {
longest := 0

for _, repo := range repos {
if len(repo.Name) > longest {
longest = len(repo.Name)
}
}

searchList := ""
for _, repo := range repos {
spaces := (longest + 5) - len(repo.Name)

text := repo.Name + strings.Repeat(" ", spaces) + repo.Path

searchList += text + "\n"
}

return searchList
}

type FzfFilter struct{}

func (f FzfFilter) Find(repos []Match) Match {
var parseName = func(line string) string {
return strings.TrimSpace(strings.Split(line, " ")[0])
}

searchList := f.formatSearch(repos)

command := fmt.Sprintf("echo '%s' | fzf", searchList)

// pipe list of repo names to fzf and get result
cmd := exec.Command("bash", "-c", command)

cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr

bts, err := cmd.Output()

if err != nil {
log.Fatal(err)
}

name := strings.TrimSpace(string(bts))

name = parseName(name)

for _, repo := range repos {

if repo.Name == name {
return repo
}
}

panic("Could not find repo")
}

0 comments on commit 0e8e6ea

Please sign in to comment.