diff --git a/README.md b/README.md index db365d1..e9e465c 100644 --- a/README.md +++ b/README.md @@ -241,9 +241,37 @@ Run `pet configure` id = "" # GitLab Snippets ID visibility = "private" # public or internal or private auto_sync = false # sync automatically when editing snippets +``` + +## Multi directory and multi file setup + +Directories musst be specified as an array. +All `toml` files will be scraped and found snippets will be added. +Example1: single directory + +``` +$ pet configure +[General] +... + snippetdirs = ["/path/to/some/snippets/"] +... ``` +Example2: multiple directories + +``` +$ pet configure +[General] +... + snippetdirs = ["/path/to/some/snippets/", "/more/snippets/"] +... +``` + If `snippetfile` setting is omitted, new snippets will be added in a seperate file to the first directory. The generated filename is time based. + +Snippet files in `snippetdirs` will not be added to Gist or GitLab. You've to do version control manually. + + ## Selector option Example1: Change layout (bottom up) diff --git a/cmd/edit.go b/cmd/edit.go index 6148c8a..d1251c8 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -1,11 +1,14 @@ package cmd import ( + "fmt" "io/ioutil" "github.com/knqyf263/pet/config" petSync "github.com/knqyf263/pet/sync" + "github.com/pkg/errors" "github.com/spf13/cobra" + "gopkg.in/alessio/shellescape.v1" ) // editCmd represents the edit command @@ -17,9 +20,26 @@ var editCmd = &cobra.Command{ } func edit(cmd *cobra.Command, args []string) (err error) { + flag := config.Flag editor := config.Conf.General.Editor snippetFile := config.Conf.General.SnippetFile + var options []string + if flag.Query != "" { + options = append(options, fmt.Sprintf("--query %s", shellescape.Quote(flag.Query))) + } + + if len(config.Conf.General.SnippetDirs) > 0 { + snippetFile, err = selectFile(options, flag.FilterTag) + if err != nil { + return err + } + } + + if snippetFile == "" { + return errors.New("No sippet file seleted") + } + // file content before editing before := fileContent(snippetFile) @@ -50,4 +70,8 @@ func fileContent(fname string) string { func init() { RootCmd.AddCommand(editCmd) + editCmd.Flags().StringVarP(&config.Flag.Query, "query", "q", "", + `Initial value for query`) + editCmd.Flags().StringVarP(&config.Flag.FilterTag, "tag", "t", "", + `Filter tag`) } diff --git a/cmd/list.go b/cmd/list.go index f749a79..bacbfe5 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -43,6 +43,10 @@ func list(cmd *cobra.Command, args []string) error { fmt.Fprintf(color.Output, "%s : %s\n", color.GreenString(description), color.YellowString(command)) } else { + if config.Flag.Debug { + fmt.Fprintf(color.Output, "%12s %s\n", + color.RedString(" Filename:"), snippet.Filename) + } fmt.Fprintf(color.Output, "%12s %s\n", color.GreenString("Description:"), snippet.Description) if strings.Contains(snippet.Command, "\n") { diff --git a/cmd/new.go b/cmd/new.go index 2369d44..758d8d8 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -66,6 +66,7 @@ func scan(message string) (string, error) { } func new(cmd *cobra.Command, args []string) (err error) { + var filename string = "" var command string var description string var tags []string @@ -103,7 +104,12 @@ func new(cmd *cobra.Command, args []string) (err error) { } } + if config.Conf.General.SnippetFile != "" { + filename = config.Conf.General.SnippetFile + } + newSnippet := snippet.SnippetInfo{ + Filename: filename, Description: description, Command: command, Tag: tags, @@ -113,9 +119,8 @@ func new(cmd *cobra.Command, args []string) (err error) { return err } - snippetFile := config.Conf.General.SnippetFile if config.Conf.Gist.AutoSync { - return petSync.AutoSync(snippetFile) + return petSync.AutoSync(filename) } return nil diff --git a/cmd/util.go b/cmd/util.go index cb40815..c7710b4 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -98,3 +98,57 @@ func filter(options []string, tag string) (commands []string, err error) { } return commands, nil } + +func selectFile(options []string, tag string) (snippetFile string, err error) { + var snippets snippet.Snippets + if err := snippets.Load(); err != nil { + return snippetFile, fmt.Errorf("Load snippet failed: %v", err) + } + + if 0 < len(tag) { + var filteredSnippets snippet.Snippets + for _, snippet := range snippets.Snippets { + for _, t := range snippet.Tag { + if tag == t { + filteredSnippets.Snippets = append(filteredSnippets.Snippets, snippet) + } + } + } + snippets = filteredSnippets + } + + snippetTexts := map[string]snippet.SnippetInfo{} + var text string + for _, s := range snippets.Snippets { + command := s.Command + if strings.ContainsAny(command, "\n") { + command = strings.Replace(command, "\n", "\\n", -1) + } + t := fmt.Sprintf("[%s]: %s", s.Description, command) + + tags := "" + for _, tag := range s.Tag { + tags += fmt.Sprintf(" #%s", tag) + } + t += tags + + snippetTexts[t] = s + text += t + "\n" + } + + var buf bytes.Buffer + selectCmd := fmt.Sprintf("%s %s", + config.Conf.General.SelectCmd, strings.Join(options, " ")) + err = run(selectCmd, strings.NewReader(text), &buf) + if err != nil { + return snippetFile, nil + } + + lines := strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n") + + for _, line := range lines { + snippetInfo := snippetTexts[line] + snippetFile = fmt.Sprint(snippetInfo.Filename) + } + return snippetFile, nil +} diff --git a/config/config.go b/config/config.go index dc0ba0c..52fe398 100644 --- a/config/config.go +++ b/config/config.go @@ -6,6 +6,7 @@ import ( "os/exec" "path/filepath" "runtime" + "github.com/BurntSushi/toml" "github.com/pkg/errors" ) @@ -15,19 +16,20 @@ var Conf Config // Config is a struct of config type Config struct { - General GeneralConfig `toml:"General"` - Gist GistConfig `toml:"Gist"` - GitLab GitLabConfig `toml:"GitLab"` + General GeneralConfig `toml:"General"` + Gist GistConfig `toml:"Gist"` + GitLab GitLabConfig `toml:"GitLab"` } // GeneralConfig is a struct of general config type GeneralConfig struct { - SnippetFile string `toml:"snippetfile"` - Editor string `toml:"editor"` - Column int `toml:"column"` - SelectCmd string `toml:"selectcmd"` - Backend string `toml:"backend"` - SortBy string `toml:"sortby"` + SnippetFile string `toml:"snippetfile"` + SnippetDirs []string `toml:"snippetdirs"` + Editor string `toml:"editor"` + Column int `toml:"column"` + SelectCmd string `toml:"selectcmd"` + Backend string `toml:"backend"` + SortBy string `toml:"sortby"` } // GistConfig is a struct of config for Gist @@ -73,7 +75,12 @@ func (cfg *Config) Load(file string) error { if err != nil { return err } + var snippetdirs []string cfg.General.SnippetFile = expandPath(cfg.General.SnippetFile) + for _, dir := range cfg.General.SnippetDirs { + snippetdirs = append(snippetdirs, expandPath(dir)) // note the = instead of := + } + cfg.General.SnippetDirs = snippetdirs return nil } diff --git a/go.mod b/go.mod index 0c52d0b..f358a38 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,10 @@ require ( golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect +) + +require ( + github.com/kennygrant/sanitize v1.2.4 gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61 ) diff --git a/go.sum b/go.sum index 750f938..94a3550 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jroimartin/gocui v0.4.0 h1:52jnalstgmc25FmtGcWqa0tcbMEWS6RpFLsOIO+I+E8= github.com/jroimartin/gocui v0.4.0/go.mod h1:7i7bbj99OgFHzo7kB2zPb8pXLqMBSQegY7azfqXMkyY= +github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= +github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= diff --git a/snippet/snippet.go b/snippet/snippet.go index b7cfe8e..db26823 100644 --- a/snippet/snippet.go +++ b/snippet/snippet.go @@ -5,8 +5,10 @@ import ( "fmt" "os" "sort" + "strings" "github.com/BurntSushi/toml" + "github.com/kennygrant/sanitize" "github.com/knqyf263/pet/config" ) @@ -15,34 +17,57 @@ type Snippets struct { } type SnippetInfo struct { + Filename string `toml:"-"` Description string `toml:"description"` Command string `toml:"command"` Tag []string `toml:"tag"` - Output string `toml:"output"` + Output string `toml:"output,omitempty"` } // Load reads toml file. func (snippets *Snippets) Load() error { - snippetFile := config.Conf.General.SnippetFile - if _, err := os.Stat(snippetFile); os.IsNotExist(err) { - return nil + var files []string + if config.Conf.General.SnippetFile != "" { + files = append(files, config.Conf.General.SnippetFile) } - if _, err := toml.DecodeFile(snippetFile, snippets); err != nil { - return fmt.Errorf("Failed to load snippet file. %v", err) + for _, dir := range config.Conf.General.SnippetDirs { + files = append(files, getFiles(dir)...) } + + for _, file := range files { + tmp := Snippets{} + if _, err := toml.DecodeFile(file, &tmp); err != nil { + return fmt.Errorf("Failed to load snippet file. %v", err) + } + for _, snippet := range tmp.Snippets { + snippet.Filename = file + snippets.Snippets = append(snippets.Snippets, snippet) + } + } + snippets.Order() return nil } // Save saves the snippets to toml file. func (snippets *Snippets) Save() error { - snippetFile := config.Conf.General.SnippetFile + var snippetFile string + var newSnippets Snippets + for _, snippet := range snippets.Snippets { + if snippet.Filename == "" { + snippetFile = config.Conf.General.SnippetDirs[0] + fmt.Sprintf("%s.toml", strings.ToLower(sanitize.BaseName(snippet.Description))) + newSnippets.Snippets = append(newSnippets.Snippets, snippet) + } else if snippet.Filename == config.Conf.General.SnippetFile { + snippetFile = config.Conf.General.SnippetFile + newSnippets.Snippets = append(newSnippets.Snippets, snippet) + } + } f, err := os.Create(snippetFile) defer f.Close() if err != nil { return fmt.Errorf("Failed to save snippet file. err: %s", err) } - return toml.NewEncoder(f).Encode(snippets) + return toml.NewEncoder(f).Encode(newSnippets) } // ToString returns the contents of toml file. diff --git a/snippet/util.go b/snippet/util.go new file mode 100644 index 0000000..099685f --- /dev/null +++ b/snippet/util.go @@ -0,0 +1,28 @@ +package snippet + +import ( + "log" + "os" + "path/filepath" + "regexp" +) + +func getFiles(path string) (fileList []string) { + tomlRegEx, err := regexp.Compile("^.+\\.(toml)$") + if err != nil { + log.Fatal(err) + } + + err = filepath.Walk(path, func(path string, f os.FileInfo, err error) error { + if err == nil && tomlRegEx.MatchString(f.Name()) { + fileList = append(fileList, path) + } + return nil + }) + + if err != nil { + panic(err) + } + + return fileList +}