Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: multiple snippet directory support #190

Merged
merged 5 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,14 @@ 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

[GHEGist]
base_url = "" # GHE base URL
Expand All @@ -269,6 +277,26 @@ Run `pet configure`
auto_sync = false # sync automatically when editing snippets

```
$ 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)
Expand Down
24 changes: 24 additions & 0 deletions cmd/edit.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package cmd

import (
"fmt"
"os"

"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
Expand All @@ -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)

Expand Down Expand Up @@ -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`)
}
4 changes: 4 additions & 0 deletions cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ func list(cmd *cobra.Command, args []string) error {
fmt.Fprintf(color.Output, "%s : %s\n",
color.HiGreenString(description), color.HiYellowString(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.HiGreenString("Description:"), snippet.Description)
if strings.Contains(snippet.Command, "\n") {
Expand Down
9 changes: 7 additions & 2 deletions cmd/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ func countSnippetLines() int {
}

func new(cmd *cobra.Command, args []string) (err error) {
var filename string = ""
var command string
var description string
var tags []string
Expand Down Expand Up @@ -240,7 +241,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,
Expand All @@ -250,9 +256,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
Expand Down
55 changes: 54 additions & 1 deletion cmd/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ func filter(options []string, tag string) (commands []string, err error) {

snippetTexts[t] = s
if config.Flag.Color || config.Conf.General.Color {
t = config.Conf.General.Format
t = strings.Replace(format, "$command", command, 1)
t = strings.Replace(t, "$description", color.HiRedString(s.Description), 1)
t = strings.Replace(t, "$tags", color.HiCyanString(tags), 1)
Expand Down Expand Up @@ -104,6 +103,60 @@ 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
}

// CountLines returns the number of lines in a certain buffer
func CountLines(r io.Reader) (int, error) {
buf := make([]byte, 32*1024)
Expand Down
10 changes: 10 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Config struct {
// GeneralConfig is a struct of general config
type GeneralConfig struct {
SnippetFile string
SnippetDirs []string
Editor string
Column int
SelectCmd string
Expand Down Expand Up @@ -88,11 +89,20 @@ func (cfg *Config) Load(file string) error {
_, err := os.Stat(file)
if err == nil {
f, err := os.ReadFile(file)
if err != nil {
return err
}

err = toml.Unmarshal(f, cfg)
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
}

Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ require (
golang.org/x/crypto v0.17.0
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
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT
github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
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/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
Expand Down
65 changes: 56 additions & 9 deletions snippet/snippet.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"fmt"
"os"
"sort"
"strings"

"github.com/kennygrant/sanitize"
"github.com/knqyf263/pet/config"
"github.com/pelletier/go-toml"
)
Expand All @@ -15,6 +17,7 @@ type Snippets struct {
}

type SnippetInfo struct {
Filename string
Description string
Command string `toml:"command,multiline"`
Tag []string
Expand All @@ -23,18 +26,51 @@ type SnippetInfo struct {

// Load reads toml file.
func (snippets *Snippets) Load() error {
var snippetFiles []string

snippetFile := config.Conf.General.SnippetFile
if _, err := os.Stat(snippetFile); os.IsNotExist(err) {
return nil
if snippetFile != "" {
if _, err := os.Stat(snippetFile); err == nil {
snippetFiles = append(snippetFiles, snippetFile)
} else if !os.IsNotExist(err) {
return fmt.Errorf("failed to load snippet file. %v", err)
} else {
return fmt.Errorf(
`snippet file not found. %s
Please run 'pet configure' and provide a correct file path, or remove this
if you only want to provide snippetdirs instead`,
snippetFile,
)
}
}
f, err := os.ReadFile(snippetFile)
if err != nil {
return fmt.Errorf("failed to load snippet file. %v", err)

for _, dir := range config.Conf.General.SnippetDirs {
if _, err := os.Stat(dir); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("snippet directory not found. %s", dir)
}
return fmt.Errorf("failed to load snippet directory. %v", err)
}
snippetFiles = append(snippetFiles, getFiles(dir)...)
}

err = toml.Unmarshal(f, snippets)
if err != nil {
return fmt.Errorf("failed to parse snippet file. %v", err)
// Read files and load snippets
for _, file := range snippetFiles {
tmp := Snippets{}
f, err := os.ReadFile(file)
if err != nil {
return fmt.Errorf("failed to load snippet file. %v", err)
}

err = toml.Unmarshal(f, &tmp)
if err != nil {
return fmt.Errorf("failed to parse snippet file. %v", err)
}

for _, snippet := range tmp.Snippets {
snippet.Filename = file
snippets.Snippets = append(snippets.Snippets, snippet)
}
}

snippets.Order()
Expand All @@ -43,11 +79,22 @@ func (snippets *Snippets) Load() error {

// 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)
if err != nil {
return fmt.Errorf("failed to save snippet file. err: %s", err)
}

defer f.Close()
return toml.NewEncoder(f).Encode(snippets)
}
Expand Down
28 changes: 28 additions & 0 deletions snippet/util.go
Original file line number Diff line number Diff line change
@@ -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
}