├── doc ├── logo.png ├── pet01.gif ├── pet02.gif ├── pet03.gif ├── pet04.gif ├── pet05.gif ├── pet06.gif └── pet08.gif ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── privileged.yml └── workflows │ ├── test.yaml │ ├── release.yaml │ └── codeql.yml ├── dialog ├── util.go ├── params.go ├── view.go └── params_test.go ├── SECURITY.md ├── Makefile ├── .gitignore ├── cmd ├── sync.go ├── util_unix.go ├── configure.go ├── util_windows.go ├── edit.go ├── exec.go ├── clip.go ├── search.go ├── list.go ├── util.go ├── root.go ├── new_test.go └── new.go ├── scripts └── package.sh ├── main.go ├── LICENSE ├── .goreleaser.yml ├── go.mod ├── misc └── completions │ └── zsh │ └── _pet ├── sync ├── sync.go ├── gist.go └── gitlab.go ├── snippet └── snippet.go ├── config └── config.go ├── go.sum └── README.md /doc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Welding-Torch/pet/HEAD/doc/logo.png -------------------------------------------------------------------------------- /doc/pet01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Welding-Torch/pet/HEAD/doc/pet01.gif -------------------------------------------------------------------------------- /doc/pet02.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Welding-Torch/pet/HEAD/doc/pet02.gif -------------------------------------------------------------------------------- /doc/pet03.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Welding-Torch/pet/HEAD/doc/pet03.gif -------------------------------------------------------------------------------- /doc/pet04.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Welding-Torch/pet/HEAD/doc/pet04.gif -------------------------------------------------------------------------------- /doc/pet05.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Welding-Torch/pet/HEAD/doc/pet05.gif -------------------------------------------------------------------------------- /doc/pet06.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Welding-Torch/pet/HEAD/doc/pet06.gif -------------------------------------------------------------------------------- /doc/pet08.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Welding-Torch/pet/HEAD/doc/pet08.gif -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /dialog/util.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | func StringInSlice(a string, list []string) bool { 4 | for _, b := range list { 5 | if b == a { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Community Support 4 | url: https://github.com/knqyf263/pet/discussions 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Reporting a Vulnerability 2 | 3 | If you discover a potential security issue in this project we ask that you notify our maintainers via email to rami.awar.ra {at} gmail.com or knqyf263 {at} gmail.com 4 | 5 | Please do **not** create a public github issue. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: \ 2 | dep \ 3 | install \ 4 | build \ 5 | vet \ 6 | test 7 | 8 | dep: 9 | go mod download 10 | 11 | build: main.go 12 | go build -o pet $< 13 | 14 | install: main.go 15 | go install 16 | 17 | test: 18 | go test ./... 19 | 20 | vet: 21 | go vet 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | .env 26 | out 27 | pkg 28 | .DS_Store 29 | .idea/ 30 | .vscode/ 31 | 32 | .glide/ 33 | vendor/ 34 | pet 35 | dist/ 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - '*.md' 8 | - 'doc/**' 9 | - 'LICENSE' 10 | pull_request: 11 | jobs: 12 | test: 13 | name: Test 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4.1.1 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version-file: go.mod 22 | 23 | - name: Run unit tests 24 | run: make test 25 | -------------------------------------------------------------------------------- /cmd/sync.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/knqyf263/pet/config" 5 | petSync "github.com/knqyf263/pet/sync" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // syncCmd represents the sync command 10 | var syncCmd = &cobra.Command{ 11 | Use: "sync", 12 | Short: "Sync snippets", 13 | Long: `Sync snippets with gist/gitlab`, 14 | RunE: sync, 15 | } 16 | 17 | func sync(cmd *cobra.Command, args []string) (err error) { 18 | return petSync.AutoSync(config.Conf.General.SnippetFile) 19 | } 20 | 21 | func init() { 22 | RootCmd.AddCommand(syncCmd) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/util_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package cmd 4 | 5 | import ( 6 | "io" 7 | "os" 8 | "os/exec" 9 | 10 | "github.com/knqyf263/pet/config" 11 | ) 12 | 13 | func run(command string, r io.Reader, w io.Writer) error { 14 | var cmd *exec.Cmd 15 | if len(config.Conf.General.Cmd) > 0 { 16 | line := append(config.Conf.General.Cmd, command) 17 | cmd = exec.Command(line[0], line[1:]...) 18 | } else { 19 | cmd = exec.Command("sh", "-c", command) 20 | } 21 | cmd.Stderr = os.Stderr 22 | cmd.Stdout = w 23 | cmd.Stdin = r 24 | return cmd.Run() 25 | } 26 | -------------------------------------------------------------------------------- /cmd/configure.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/knqyf263/pet/config" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | // configureCmd represents the configure command 9 | var configureCmd = &cobra.Command{ 10 | Use: "configure", 11 | Short: "Edit config file", 12 | Long: `Edit config file (default: opened by vim)`, 13 | RunE: configure, 14 | } 15 | 16 | func configure(cmd *cobra.Command, args []string) (err error) { 17 | editor := config.Conf.General.Editor 18 | return editFile(editor, configFile) 19 | } 20 | 21 | func init() { 22 | RootCmd.AddCommand(configureCmd) 23 | } 24 | -------------------------------------------------------------------------------- /scripts/package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | set -x 5 | 6 | VERSION=$(grep "version = " cmd/root.go | sed -E 's/.*"(.+)"$/\1/') 7 | REPO="pet" 8 | 9 | rm -rf ./out/ 10 | gox --osarch "windows/386 windows/amd64 darwin/386 darwin/amd64 linux/386 linux/amd64" -output="./out/${REPO}_${VERSION}_{{.OS}}_{{.Arch}}/{{.Dir}}" 11 | 12 | rm -rf ./pkg/ 13 | mkdir ./pkg 14 | 15 | for PLATFORM in $(find ./out -mindepth 1 -maxdepth 1 -type d); do 16 | PLATFORM_NAME=$(basename ${PLATFORM}) 17 | 18 | pushd ${PLATFORM} 19 | cp -r ../../misc ./ 20 | zip -r ../../pkg/${PLATFORM_NAME}.zip ./* 21 | popd 22 | done 23 | -------------------------------------------------------------------------------- /cmd/util_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package cmd 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "syscall" 11 | 12 | "github.com/knqyf263/pet/config" 13 | ) 14 | 15 | func run(command string, r io.Reader, w io.Writer) error { 16 | var cmd *exec.Cmd 17 | if len(config.Conf.General.Cmd) > 0 { 18 | line := append(config.Conf.General.Cmd, command) 19 | cmd = exec.Command(line[0], line[1:]...) 20 | } else { 21 | cmd = exec.Command("cmd.exe") 22 | cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: fmt.Sprintf("/c \"%s\"", command)} 23 | } 24 | cmd.Stderr = os.Stderr 25 | cmd.Stdout = w 26 | cmd.Stdin = r 27 | return cmd.Run() 28 | } 29 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Teppei Fukuda 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import "github.com/knqyf263/pet/cmd" 18 | 19 | func main() { 20 | cmd.Execute() 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-20.04 10 | steps: 11 | - name: Install dependencies 12 | run: | 13 | sudo apt-get -y update 14 | sudo apt-get -y install rpm 15 | - name: Checkout code 16 | uses: actions/checkout@v4.1.1 17 | with: 18 | fetch-depth: 0 19 | - name: Setup Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version-file: go.mod 23 | - name: Release 24 | uses: goreleaser/goreleaser-action@v5 25 | with: 26 | version: v1.18.2 27 | args: release --clean 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.PET_PAT }} 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Teppei Fukuda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/edit.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/knqyf263/pet/config" 7 | petSync "github.com/knqyf263/pet/sync" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // editCmd represents the edit command 12 | var editCmd = &cobra.Command{ 13 | Use: "edit", 14 | Short: "Edit snippet file", 15 | Long: `Edit snippet file (default: opened by vim)`, 16 | RunE: edit, 17 | } 18 | 19 | func edit(cmd *cobra.Command, args []string) (err error) { 20 | editor := config.Conf.General.Editor 21 | snippetFile := config.Conf.General.SnippetFile 22 | 23 | // file content before editing 24 | before := fileContent(snippetFile) 25 | 26 | err = editFile(editor, snippetFile) 27 | if err != nil { 28 | return 29 | } 30 | 31 | // file content after editing 32 | after := fileContent(snippetFile) 33 | 34 | // return if same file content 35 | if before == after { 36 | return nil 37 | } 38 | 39 | if config.Conf.Gist.AutoSync { 40 | return petSync.AutoSync(snippetFile) 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func fileContent(fname string) string { 47 | data, _ := os.ReadFile(fname) 48 | return string(data) 49 | } 50 | 51 | func init() { 52 | RootCmd.AddCommand(editCmd) 53 | } 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/privileged.yml: -------------------------------------------------------------------------------- 1 | name: Privileged 2 | description: You were directed from Github discussions to create an issue in Pet 👇 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | ## Thanks! 8 | Thanks for your interest in Pet! 🚀 9 | If you are were not directed to create an issue here, please start the conversation in a [Question in GitHub Discussions](https://github.com/knqyf263/pet/discussions/categories/questions) instead! 10 | 11 | ## Got a PR? 12 | If you want to create a PR, share the idea first in [Github discussions](https://github.com/knqyf263/pet/discussions)! 💬 13 | This will give you ⚡️ quick feedback ⚡️ before you've spent time on it, instead of waiting for a much longer review process AFTER you've done the work. 14 | - type: checkboxes 15 | id: privileged 16 | attributes: 17 | label: Privileged issue 18 | description: Confirm that you were directed to create an issue here. 19 | options: 20 | - label: I've been directed through Discussions to create an issue here. 21 | required: true 22 | - type: textarea 23 | id: content 24 | attributes: 25 | label: Issue Content 26 | description: Add the content of the issue here. 27 | 28 | -------------------------------------------------------------------------------- /cmd/exec.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/knqyf263/pet/config" 9 | "github.com/spf13/cobra" 10 | "gopkg.in/alessio/shellescape.v1" 11 | ) 12 | 13 | // execCmd represents the exec command 14 | var execCmd = &cobra.Command{ 15 | Use: "exec", 16 | Short: "Run the selected commands", 17 | Long: `Run the selected commands directly`, 18 | RunE: execute, 19 | } 20 | 21 | func execute(cmd *cobra.Command, args []string) (err error) { 22 | flag := config.Flag 23 | 24 | var options []string 25 | if flag.Query != "" { 26 | options = append(options, fmt.Sprintf("--query %s", shellescape.Quote(flag.Query))) 27 | } 28 | 29 | commands, err := filter(options, flag.FilterTag) 30 | if err != nil { 31 | return err 32 | } 33 | command := strings.Join(commands, "; ") 34 | 35 | // Show final command before executing it 36 | fmt.Printf("> %s\n", command) 37 | 38 | return run(command, os.Stdin, os.Stdout) 39 | } 40 | 41 | func init() { 42 | RootCmd.AddCommand(execCmd) 43 | execCmd.Flags().BoolVarP(&config.Flag.Color, "color", "", false, 44 | `Enable colorized output (only fzf)`) 45 | execCmd.Flags().StringVarP(&config.Flag.Query, "query", "q", "", 46 | `Initial value for query`) 47 | execCmd.Flags().StringVarP(&config.Flag.FilterTag, "tag", "t", "", 48 | `Filter tag`) 49 | } 50 | -------------------------------------------------------------------------------- /cmd/clip.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/atotto/clipboard" 8 | "github.com/fatih/color" 9 | "github.com/knqyf263/pet/config" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // clipCmd represents the clip command 14 | var clipCmd = &cobra.Command{ 15 | Use: "clip", 16 | Short: "Copy the selected commands", 17 | Long: `Copy the selected commands to clipboard`, 18 | RunE: clip, 19 | } 20 | 21 | func clip(cmd *cobra.Command, args []string) (err error) { 22 | flag := config.Flag 23 | 24 | var options []string 25 | if flag.Query != "" { 26 | options = append(options, fmt.Sprintf("--query %s", flag.Query)) 27 | } 28 | 29 | commands, err := filter(options, flag.FilterTag) 30 | if err != nil { 31 | return err 32 | } 33 | command := strings.Join(commands, flag.Delimiter) 34 | if flag.Command && command != "" { 35 | fmt.Printf("%s: %s\n", color.YellowString("Command"), command) 36 | } 37 | return clipboard.WriteAll(command) 38 | } 39 | 40 | func init() { 41 | RootCmd.AddCommand(clipCmd) 42 | clipCmd.Flags().StringVarP(&config.Flag.Query, "query", "q", "", 43 | `Initial value for query`) 44 | clipCmd.Flags().BoolVarP(&config.Flag.Command, "command", "", false, 45 | `Display snippets in one line`) 46 | clipCmd.Flags().StringVarP(&config.Flag.Delimiter, "delimiter", "d", "; ", 47 | `Use delim as the command delimiter character`) 48 | clipCmd.Flags().StringVarP(&config.Flag.FilterTag, "tag", "t", "", 49 | `Filter tag`) 50 | } 51 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | release: 2 | github: 3 | owner: knqyf263 4 | name: pet 5 | name_template: '{{.Tag}}' 6 | brews: 7 | - 8 | tap: 9 | owner: knqyf263 10 | name: homebrew-pet 11 | commit_author: 12 | name: goreleaserbot 13 | email: goreleaser@carlosbecker.com 14 | description: "Simple command-line snippet manager" 15 | homepage: "https://github.com/knqyf263/pet" 16 | dependencies: 17 | - fzf 18 | install: | 19 | bin.install Dir['pet'] 20 | zsh_completion.install "misc/completions/zsh/_pet" 21 | test: | 22 | system "#{bin}/pet" 23 | builds: 24 | - goos: 25 | - linux 26 | - darwin 27 | - windows 28 | goarch: 29 | - amd64 30 | - arm 31 | - arm64 32 | - "386" 33 | goarm: 34 | - "6" 35 | main: . 36 | ldflags: -s -w -X github.com/knqyf263/pet/cmd.version={{.Version}} 37 | archives: 38 | - 39 | format: tar.gz 40 | name_template: '{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ 41 | .Arm }}{{ end }}' 42 | files: 43 | - LICENSE* 44 | - README* 45 | - CHANGELOG* 46 | - misc/completions/zsh/_pet 47 | nfpms: 48 | - 49 | homepage: https://github.com/knqyf263/pet 50 | maintainer: Teppei Fukuda 51 | description: "Simple command-line snippet manager" 52 | bindir: /usr/local/bin 53 | license: MIT 54 | formats: 55 | - deb 56 | - rpm 57 | checksum: 58 | name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt' 59 | -------------------------------------------------------------------------------- /cmd/search.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/knqyf263/pet/config" 8 | "github.com/spf13/cobra" 9 | "golang.org/x/crypto/ssh/terminal" 10 | "gopkg.in/alessio/shellescape.v1" 11 | ) 12 | 13 | var delimiter string 14 | 15 | // searchCmd represents the search command 16 | var searchCmd = &cobra.Command{ 17 | Use: "search", 18 | Short: "Search snippets", 19 | Long: `Search snippets interactively (default filtering tool: peco)`, 20 | RunE: search, 21 | } 22 | 23 | func search(cmd *cobra.Command, args []string) (err error) { 24 | flag := config.Flag 25 | 26 | var options []string 27 | if flag.Query != "" { 28 | options = append(options, fmt.Sprintf("--query %s", shellescape.Quote(flag.Query))) 29 | } 30 | commands, err := filter(options, flag.FilterTag) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | fmt.Print(strings.Join(commands, flag.Delimiter)) 36 | if terminal.IsTerminal(1) { 37 | fmt.Print("\n") 38 | } 39 | return nil 40 | } 41 | 42 | func init() { 43 | RootCmd.AddCommand(searchCmd) 44 | searchCmd.Flags().BoolVarP(&config.Flag.Color, "color", "", false, 45 | `Enable colorized output (only fzf)`) 46 | searchCmd.Flags().StringVarP(&config.Flag.Query, "query", "q", "", 47 | `Initial value for query`) 48 | searchCmd.Flags().StringVarP(&config.Flag.FilterTag, "tag", "t", "", 49 | `Filter tag`) 50 | searchCmd.Flags().StringVarP(&config.Flag.Delimiter, "delimiter", "d", "; ", 51 | `Use delim as the command delimiter character`) 52 | } 53 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/knqyf263/pet 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.0 7 | github.com/atotto/clipboard v0.1.4 8 | github.com/briandowns/spinner v0.0.0-20170614154858-48dbb65d7bd5 9 | github.com/chzyer/logex v1.1.10 // indirect 10 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e 11 | github.com/chzyer/test v0.0.0-20210722231415-061457976a23 // indirect 12 | github.com/fatih/color v1.7.0 13 | github.com/google/go-github v15.0.0+incompatible 14 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 15 | github.com/mattn/go-runewidth v0.0.10 16 | github.com/pkg/errors v0.8.0 17 | github.com/spf13/cobra v0.0.3 18 | github.com/spf13/pflag v1.0.1 // indirect 19 | github.com/xanzy/go-gitlab v0.50.3 20 | //github.com/xanzy/go-gitlab v0.10.5 21 | golang.org/x/crypto v0.17.0 22 | golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 23 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 24 | gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61 25 | ) 26 | 27 | require ( 28 | github.com/awesome-gocui/gocui v1.1.0 29 | github.com/go-test/deep v1.1.0 30 | ) 31 | 32 | require ( 33 | github.com/alessio/shellescape v1.4.1 // indirect 34 | github.com/gdamore/encoding v1.0.0 // indirect 35 | github.com/gdamore/tcell/v2 v2.4.0 // indirect 36 | github.com/golang/protobuf v1.2.0 // indirect 37 | github.com/google/go-querystring v1.0.0 // indirect 38 | github.com/hashicorp/go-cleanhttp v0.5.1 // indirect 39 | github.com/hashicorp/go-retryablehttp v0.6.8 // indirect 40 | github.com/lucasb-eyer/go-colorful v1.0.3 // indirect 41 | github.com/mattn/go-colorable v0.0.9 // indirect 42 | github.com/mattn/go-isatty v0.0.3 // indirect 43 | github.com/rivo/uniseg v0.1.0 // indirect 44 | golang.org/x/net v0.17.0 // indirect 45 | golang.org/x/sys v0.15.0 // indirect 46 | golang.org/x/term v0.15.0 // indirect 47 | golang.org/x/text v0.14.0 // indirect 48 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect 49 | google.golang.org/appengine v1.3.0 // indirect 50 | ) 51 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/fatih/color" 8 | "github.com/knqyf263/pet/config" 9 | "github.com/knqyf263/pet/snippet" 10 | runewidth "github.com/mattn/go-runewidth" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | const ( 15 | column = 40 16 | ) 17 | 18 | // listCmd represents the list command 19 | var listCmd = &cobra.Command{ 20 | Use: "list", 21 | Short: "Show all snippets", 22 | Long: `Show all snippets`, 23 | RunE: list, 24 | } 25 | 26 | func list(cmd *cobra.Command, args []string) error { 27 | var snippets snippet.Snippets 28 | if err := snippets.Load(); err != nil { 29 | return err 30 | } 31 | 32 | col := config.Conf.General.Column 33 | if col == 0 { 34 | col = column 35 | } 36 | 37 | for _, snippet := range snippets.Snippets { 38 | if config.Flag.OneLine { 39 | description := runewidth.FillRight(runewidth.Truncate(snippet.Description, col, "..."), col) 40 | command := runewidth.Truncate(snippet.Command, 100-4-col, "...") 41 | // make sure multiline command printed as oneline 42 | command = strings.Replace(command, "\n", "\\n", -1) 43 | fmt.Fprintf(color.Output, "%s : %s\n", 44 | color.HiGreenString(description), color.HiYellowString(command)) 45 | } else { 46 | fmt.Fprintf(color.Output, "%12s %s\n", 47 | color.HiGreenString("Description:"), snippet.Description) 48 | if strings.Contains(snippet.Command, "\n") { 49 | lines := strings.Split(snippet.Command, "\n") 50 | firstLine, restLines := lines[0], lines[1:] 51 | fmt.Fprintf(color.Output, "%12s %s\n", 52 | color.HiYellowString(" Command:"), firstLine) 53 | for _, line := range restLines { 54 | fmt.Fprintf(color.Output, "%12s %s\n", 55 | " ", line) 56 | } 57 | } else { 58 | fmt.Fprintf(color.Output, "%12s %s\n", 59 | color.HiYellowString(" Command:"), snippet.Command) 60 | } 61 | if snippet.Tag != nil { 62 | tag := strings.Join(snippet.Tag, " ") 63 | fmt.Fprintf(color.Output, "%12s %s\n", 64 | color.HiCyanString(" Tag:"), tag) 65 | } 66 | if snippet.Output != "" { 67 | output := strings.Replace(snippet.Output, "\n", "\n ", -1) 68 | fmt.Fprintf(color.Output, "%12s %s\n", 69 | color.HiRedString(" Output:"), output) 70 | } 71 | fmt.Println(strings.Repeat("-", 30)) 72 | } 73 | } 74 | return nil 75 | } 76 | 77 | func init() { 78 | RootCmd.AddCommand(listCmd) 79 | listCmd.Flags().BoolVarP(&config.Flag.OneLine, "oneline", "", false, 80 | `Display snippets in one line`) 81 | } 82 | -------------------------------------------------------------------------------- /cmd/util.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/fatih/color" 10 | "github.com/knqyf263/pet/config" 11 | "github.com/knqyf263/pet/dialog" 12 | "github.com/knqyf263/pet/snippet" 13 | ) 14 | 15 | func editFile(command, file string) error { 16 | command += " " + file 17 | return run(command, os.Stdin, os.Stdout) 18 | } 19 | 20 | func filter(options []string, tag string) (commands []string, err error) { 21 | var snippets snippet.Snippets 22 | if err := snippets.Load(); err != nil { 23 | return commands, fmt.Errorf("Load snippet failed: %v", err) 24 | } 25 | 26 | if 0 < len(tag) { 27 | var filteredSnippets snippet.Snippets 28 | for _, snippet := range snippets.Snippets { 29 | for _, t := range snippet.Tag { 30 | if tag == t { 31 | filteredSnippets.Snippets = append(filteredSnippets.Snippets, snippet) 32 | } 33 | } 34 | } 35 | snippets = filteredSnippets 36 | } 37 | 38 | snippetTexts := map[string]snippet.SnippetInfo{} 39 | var text string 40 | for _, s := range snippets.Snippets { 41 | command := s.Command 42 | if strings.ContainsAny(command, "\n") { 43 | command = strings.Replace(command, "\n", "\\n", -1) 44 | } 45 | t := fmt.Sprintf("[%s]: %s", s.Description, command) 46 | 47 | tags := "" 48 | for _, tag := range s.Tag { 49 | tags += fmt.Sprintf(" #%s", tag) 50 | } 51 | t += tags 52 | 53 | snippetTexts[t] = s 54 | if config.Flag.Color { 55 | t = fmt.Sprintf("[%s]: %s%s", 56 | color.HiRedString(s.Description), command, color.HiCyanString(tags)) 57 | } 58 | text += t + "\n" 59 | } 60 | 61 | var buf bytes.Buffer 62 | selectCmd := fmt.Sprintf("%s %s", 63 | config.Conf.General.SelectCmd, strings.Join(options, " ")) 64 | err = run(selectCmd, strings.NewReader(text), &buf) 65 | if err != nil { 66 | return nil, nil 67 | } 68 | 69 | lines := strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n") 70 | var params [][2]string 71 | 72 | // If only one line is selected, search for params in the command 73 | if len(lines) == 1 { 74 | snippetInfo := snippetTexts[lines[0]] 75 | params = dialog.SearchForParams(snippetInfo.Command) 76 | } else { 77 | params = nil 78 | } 79 | 80 | if params != nil { 81 | snippetInfo := snippetTexts[lines[0]] 82 | dialog.CurrentCommand = snippetInfo.Command 83 | dialog.GenerateParamsLayout(params, dialog.CurrentCommand) 84 | res := []string{dialog.FinalCommand} 85 | return res, nil 86 | } 87 | for _, line := range lines { 88 | snippetInfo := snippetTexts[line] 89 | commands = append(commands, fmt.Sprint(snippetInfo.Command)) 90 | } 91 | return commands, nil 92 | } 93 | -------------------------------------------------------------------------------- /misc/completions/zsh/_pet: -------------------------------------------------------------------------------- 1 | #compdef pet 2 | # vim: ft=zsh 3 | 4 | _pet () { 5 | local -a _1st_arguments 6 | _1st_arguments=( 7 | 'configure:Edit config file' 8 | 'edit:Edit snippet file' 9 | 'exec:Run the selected commands' 10 | 'help:Help about any command' 11 | 'list:Show all snippets' 12 | 'new:Create a new snippet' 13 | 'search:Search snippets' 14 | 'sync:Sync snippets' 15 | 'version:Print the version number' 16 | ) 17 | 18 | _arguments \ 19 | '(--help)--help[show this help message]' \ 20 | '(--config)--config=[config file (default is $HOME/.config/pet/config.toml)]' \ 21 | '(--debug)--debug[debug mode]' \ 22 | '*:: :->subcmds' \ 23 | && return 0 24 | 25 | if (( CURRENT == 1 )); then 26 | _describe -t commands "pet subcommand" _1st_arguments 27 | return 28 | fi 29 | 30 | case "$words[1]" in 31 | ("configure"|"edit"|"version") 32 | _arguments \ 33 | '(- :)'{-h,--help}'[Show this help and exit]' \ 34 | && return 0 35 | ;; 36 | ("exec") 37 | _arguments \ 38 | '(- :)'{-h,--help}'[Show this help and exit]' \ 39 | '(--color)--color[Enable colorized output (only fzf)]' \ 40 | '(-q --query)'{-q,--query}'=[Initial value for query]' \ 41 | && return 0 42 | ;; 43 | ("list") 44 | _arguments \ 45 | '(- :)'{-h,--help}'[Show this help and exit]' \ 46 | '(--oneline)--oneline[Display snippets in one line]' \ 47 | && return 0 48 | ;; 49 | ("new") 50 | _arguments \ 51 | '(- :)'{-h,--help}'[Show this help and exit]' \ 52 | '(-t --tag)'{-t,--tag}'=[Display tag prompt (delimiter: space)]' \ 53 | && return 0 54 | ;; 55 | ("search") 56 | _arguments \ 57 | '(- :)'{-h,--help}'[Show this help and exit]' \ 58 | '(--color)--color[Enable colorized output (only fzf)]' \ 59 | '(-d --delimiter)'{-d,--delimiter}'[Use delim as the command delimiter character (default "; ")]' \ 60 | '(-q --query)'{-q,--query}'=[Initial value for query]' \ 61 | && return 0 62 | ;; 63 | ("sync") 64 | _arguments \ 65 | '(- :)'{-h,--help}'[Show this help and exit]' \ 66 | '(-u --upload)'{-u,--upload}'[Upload snippets to gist]' \ 67 | && return 0 68 | ;; 69 | ("help") 70 | _values 'help message' ${_1st_arguments[@]%:*} && return 0 71 | ;; 72 | esac 73 | } 74 | 75 | _pet "$@" 76 | -------------------------------------------------------------------------------- /sync/sync.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/knqyf263/pet/config" 9 | "github.com/knqyf263/pet/snippet" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // Client manages communication with the remote Snippet repository 14 | type Client interface { 15 | GetSnippet() (*Snippet, error) 16 | UploadSnippet(string) error 17 | } 18 | 19 | // Snippet is the remote snippet 20 | type Snippet struct { 21 | Content string 22 | UpdatedAt time.Time 23 | } 24 | 25 | // AutoSync syncs snippets automatically 26 | func AutoSync(file string) error { 27 | client, err := NewSyncClient() 28 | if err != nil { 29 | return errors.Wrap(err, "Failed to initialize API client") 30 | } 31 | 32 | snippet, err := client.GetSnippet() 33 | if err != nil { 34 | return err 35 | } 36 | 37 | fi, err := os.Stat(file) 38 | if os.IsNotExist(err) || fi.Size() == 0 { 39 | return download(snippet.Content) 40 | } else if err != nil { 41 | return errors.Wrap(err, "Failed to get a FileInfo") 42 | } 43 | 44 | local := fi.ModTime().UTC() 45 | remote := snippet.UpdatedAt.UTC() 46 | 47 | switch { 48 | case local.After(remote): 49 | return upload(client) 50 | case remote.After(local): 51 | return download(snippet.Content) 52 | default: 53 | return nil 54 | } 55 | } 56 | 57 | // NewSyncClient returns Client 58 | func NewSyncClient() (Client, error) { 59 | if config.Conf.General.Backend == "gitlab" { 60 | client, err := NewGitLabClient() 61 | if err != nil { 62 | return nil, errors.Wrap(err, "Failed to initialize GitLab client") 63 | } 64 | return client, nil 65 | } 66 | client, err := NewGistClient() 67 | if err != nil { 68 | return nil, errors.Wrap(err, "Failed to initialize Gist client") 69 | } 70 | return client, nil 71 | } 72 | 73 | func upload(client Client) (err error) { 74 | var snippets snippet.Snippets 75 | if err := snippets.Load(); err != nil { 76 | return errors.Wrap(err, "Failed to load the local snippets") 77 | } 78 | 79 | body, err := snippets.ToString() 80 | if err != nil { 81 | return err 82 | } 83 | 84 | if err = client.UploadSnippet(body); err != nil { 85 | return errors.Wrap(err, "Failed to upload snippet") 86 | } 87 | 88 | fmt.Println("Upload success") 89 | return nil 90 | } 91 | 92 | func download(content string) error { 93 | snippetFile := config.Conf.General.SnippetFile 94 | 95 | var snippets snippet.Snippets 96 | if err := snippets.Load(); err != nil { 97 | return err 98 | } 99 | body, err := snippets.ToString() 100 | if err != nil { 101 | return err 102 | } 103 | if content == body { 104 | // no need to download 105 | fmt.Println("Already up-to-date") 106 | return nil 107 | } 108 | 109 | fmt.Println("Download success") 110 | return os.WriteFile(snippetFile, []byte(content), os.ModePerm) 111 | } 112 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Teppei Fukuda 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cmd 22 | 23 | import ( 24 | "fmt" 25 | "os" 26 | "path/filepath" 27 | 28 | "github.com/knqyf263/pet/config" 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | var ( 33 | configFile string 34 | version = "dev" 35 | ) 36 | 37 | // RootCmd represents the base command when called without any subcommands 38 | var RootCmd = &cobra.Command{ 39 | Use: "pet", 40 | Short: "Simple command-line snippet manager.", 41 | Long: `pet - Simple command-line snippet manager.`, 42 | SilenceErrors: true, 43 | SilenceUsage: true, 44 | } 45 | 46 | // Execute adds all child commands to the root command sets flags appropriately. 47 | func Execute() { 48 | if err := RootCmd.Execute(); err != nil { 49 | fmt.Println(err) 50 | os.Exit(-1) 51 | } 52 | } 53 | 54 | func init() { 55 | cobra.OnInitialize(initConfig) 56 | RootCmd.AddCommand(versionCmd) 57 | 58 | RootCmd.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is $HOME/.config/pet/config.toml)") 59 | RootCmd.PersistentFlags().BoolVarP(&config.Flag.Debug, "debug", "", false, "debug mode") 60 | } 61 | 62 | var versionCmd = &cobra.Command{ 63 | Use: "version", 64 | Short: "Print the version number", 65 | Long: `Print the version number`, 66 | Run: func(cmd *cobra.Command, args []string) { 67 | fmt.Printf("pet version %s\n", version) 68 | }, 69 | } 70 | 71 | // initConfig reads in config file and ENV variables if set. 72 | func initConfig() { 73 | if configFile == "" { 74 | dir, err := config.GetDefaultConfigDir() 75 | if err != nil { 76 | fmt.Fprintf(os.Stderr, "%v", err) 77 | os.Exit(1) 78 | } 79 | configFile = filepath.Join(dir, "config.toml") 80 | } 81 | 82 | if err := config.Conf.Load(configFile); err != nil { 83 | fmt.Fprintf(os.Stderr, "%v", err) 84 | os.Exit(1) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cmd/new_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | // MockReadCloser is a mock implementation of io.ReadCloser 10 | type MockReadCloser struct { 11 | *strings.Reader 12 | } 13 | 14 | // Close does nothing for this mock implementation 15 | func (m *MockReadCloser) Close() error { 16 | return nil 17 | } 18 | 19 | func TestScan(t *testing.T) { 20 | message := "Enter something: " 21 | 22 | input := "test\n" // Simulated user input 23 | want := "test" // Expected output 24 | expectedError := error(nil) 25 | 26 | // Create a buffer for output 27 | var outputBuffer bytes.Buffer 28 | // Create a mock ReadCloser for input 29 | inputReader := &MockReadCloser{strings.NewReader(input)} 30 | 31 | result, err := scan(message, &outputBuffer, inputReader, false) 32 | 33 | // Check if the input was printed 34 | got := result 35 | 36 | // Check if the result matches the expected result 37 | if want != got { 38 | t.Errorf("Expected result %q, but got %q", want, got) 39 | } 40 | 41 | // Check if the error matches the expected error 42 | if err != expectedError { 43 | t.Errorf("Expected error %v, but got %v", expectedError, err) 44 | } 45 | } 46 | 47 | func TestScan_EmptyStringWithAllowEmpty(t *testing.T) { 48 | message := "Enter something: " 49 | 50 | input := "\n" // Simulated user input 51 | want := "" // Expected output 52 | expectedError := error(nil) 53 | 54 | // Create a buffer for output 55 | var outputBuffer bytes.Buffer 56 | // Create a mock ReadCloser for input 57 | inputReader := &MockReadCloser{strings.NewReader(input)} 58 | 59 | result, err := scan(message, &outputBuffer, inputReader, true) 60 | 61 | // Check if the input was printed 62 | got := result 63 | 64 | // Check if the result matches the expected result 65 | if want != got { 66 | t.Errorf("Expected result %q, but got %q", want, got) 67 | } 68 | 69 | // Check if the error matches the expected error 70 | if err != expectedError { 71 | t.Errorf("Expected error %v, but got %v", expectedError, err) 72 | } 73 | } 74 | 75 | func TestScan_EmptyStringWithoutAllowEmpty(t *testing.T) { 76 | message := "Enter something: " 77 | 78 | input := "\n" // Simulated user input 79 | want := "" // Expected output 80 | expectedError := CanceledError() 81 | 82 | // Create a buffer for output 83 | var outputBuffer bytes.Buffer 84 | // Create a mock ReadCloser for input 85 | inputReader := &MockReadCloser{strings.NewReader(input)} 86 | 87 | result, err := scan(message, &outputBuffer, inputReader, false) 88 | 89 | // Check if the input was printed 90 | got := result 91 | 92 | // Check if the result matches the expected result 93 | if want != got { 94 | t.Errorf("Expected result %q, but got %q", want, got) 95 | } 96 | 97 | // Check if the error matches the expected error 98 | if err.Error() != expectedError.Error() { 99 | t.Errorf("Expected error %v, but got %v", expectedError, err) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /dialog/params.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/awesome-gocui/gocui" 8 | ) 9 | 10 | var ( 11 | views = []string{} 12 | 13 | //CurrentCommand is the command before assigning to variables 14 | CurrentCommand string 15 | //FinalCommand is the command after assigning to variables 16 | FinalCommand string 17 | 18 | // This matches most encountered patterns 19 | // Skips match if there is a whitespace at the end ex. 20 | // Ignores <, > characters since they're used to match the pattern 21 | parameterStringRegex = `<([^<>]*[^\s])>` 22 | ) 23 | 24 | func insertParams(command string, filledInParams map[string]string) string { 25 | r := regexp.MustCompile(parameterStringRegex) 26 | 27 | matches := r.FindAllStringSubmatch(command, -1) 28 | if len(matches) == 0 { 29 | return command 30 | } 31 | 32 | resultCommand := command 33 | 34 | // First match is the whole match (with brackets), second is the first group 35 | // Ex. echo 36 | // -> matches[0][0]: 37 | // -> matches[0][1]: param='my param' 38 | for _, p := range matches { 39 | whole, matchedGroup := p[0], p[1] 40 | param, _, _ := strings.Cut(matchedGroup, "=") 41 | 42 | // Replace the whole match with the filled-in value of the param 43 | resultCommand = strings.Replace(resultCommand, whole, filledInParams[param], -1) 44 | } 45 | 46 | return resultCommand 47 | } 48 | 49 | // SearchForParams returns variables from a command 50 | func SearchForParams(command string) [][2]string { 51 | r := regexp.MustCompile(parameterStringRegex) 52 | 53 | params := r.FindAllStringSubmatch(command, -1) 54 | if len(params) == 0 { 55 | return nil 56 | } 57 | 58 | extracted := map[string]string{} 59 | ordered_params := [][2]string{} 60 | for _, p := range params { 61 | _, matchedGroup := p[0], p[1] 62 | paramKey, defaultValue, separatorFound := strings.Cut(matchedGroup, "=") 63 | _, param_exists := extracted[paramKey] 64 | 65 | // Set to empty if no value is provided and param is not already set 66 | if !separatorFound && !param_exists { 67 | extracted[paramKey] = "" 68 | } else if separatorFound { 69 | // Set to default value instead if it is provided 70 | extracted[paramKey] = defaultValue 71 | } 72 | 73 | // Fill in the keys only if seen for the first time to track order 74 | if !param_exists { 75 | ordered_params = append(ordered_params, [2]string{paramKey, ""}) 76 | } 77 | } 78 | 79 | // Fill in the values 80 | for i, param := range ordered_params { 81 | pair := [2]string{param[0], extracted[param[0]]} 82 | ordered_params[i] = pair 83 | } 84 | return ordered_params 85 | } 86 | 87 | func evaluateParams(g *gocui.Gui, _ *gocui.View) error { 88 | paramsFilled := map[string]string{} 89 | for _, v := range views { 90 | view, _ := g.View(v) 91 | res := view.Buffer() 92 | res = strings.Replace(res, "\n", "", -1) 93 | paramsFilled[v] = strings.TrimSpace(res) 94 | } 95 | FinalCommand = insertParams(CurrentCommand, paramsFilled) 96 | return gocui.ErrQuit 97 | } 98 | -------------------------------------------------------------------------------- /snippet/snippet.go: -------------------------------------------------------------------------------- 1 | package snippet 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "sort" 8 | 9 | "github.com/BurntSushi/toml" 10 | "github.com/knqyf263/pet/config" 11 | ) 12 | 13 | type Snippets struct { 14 | Snippets []SnippetInfo `toml:"snippets"` 15 | } 16 | 17 | type SnippetInfo struct { 18 | Description string `toml:"description"` 19 | Command string `toml:"command"` 20 | Tag []string `toml:"tag"` 21 | Output string `toml:"output"` 22 | } 23 | 24 | // Load reads toml file. 25 | func (snippets *Snippets) Load() error { 26 | snippetFile := config.Conf.General.SnippetFile 27 | if _, err := os.Stat(snippetFile); os.IsNotExist(err) { 28 | return nil 29 | } 30 | if _, err := toml.DecodeFile(snippetFile, snippets); err != nil { 31 | return fmt.Errorf("Failed to load snippet file. %v", err) 32 | } 33 | snippets.Order() 34 | return nil 35 | } 36 | 37 | // Save saves the snippets to toml file. 38 | func (snippets *Snippets) Save() error { 39 | snippetFile := config.Conf.General.SnippetFile 40 | f, err := os.Create(snippetFile) 41 | defer f.Close() 42 | if err != nil { 43 | return fmt.Errorf("Failed to save snippet file. err: %s", err) 44 | } 45 | return toml.NewEncoder(f).Encode(snippets) 46 | } 47 | 48 | // ToString returns the contents of toml file. 49 | func (snippets *Snippets) ToString() (string, error) { 50 | var buffer bytes.Buffer 51 | err := toml.NewEncoder(&buffer).Encode(snippets) 52 | if err != nil { 53 | return "", fmt.Errorf("Failed to convert struct to TOML string: %v", err) 54 | } 55 | return buffer.String(), nil 56 | } 57 | 58 | // Order snippets regarding SortBy option defined in config toml 59 | // Prefix "-" reverses the order, default is "recency", "+" is the same as "" 60 | func (snippets *Snippets) Order() { 61 | sortBy := config.Conf.General.SortBy 62 | switch { 63 | case sortBy == "command" || sortBy == "+command": 64 | sort.Sort(ByCommand(snippets.Snippets)) 65 | case sortBy == "-command": 66 | sort.Sort(sort.Reverse(ByCommand(snippets.Snippets))) 67 | 68 | case sortBy == "description" || sortBy == "+description": 69 | sort.Sort(ByDescription(snippets.Snippets)) 70 | case sortBy == "-description": 71 | sort.Sort(sort.Reverse(ByDescription(snippets.Snippets))) 72 | 73 | case sortBy == "output" || sortBy == "+output": 74 | sort.Sort(ByOutput(snippets.Snippets)) 75 | case sortBy == "-output": 76 | sort.Sort(sort.Reverse(ByOutput(snippets.Snippets))) 77 | 78 | case sortBy == "-recency": 79 | snippets.reverse() 80 | } 81 | } 82 | 83 | func (snippets *Snippets) reverse() { 84 | for i, j := 0, len(snippets.Snippets)-1; i < j; i, j = i+1, j-1 { 85 | snippets.Snippets[i], snippets.Snippets[j] = snippets.Snippets[j], snippets.Snippets[i] 86 | } 87 | } 88 | 89 | type ByCommand []SnippetInfo 90 | 91 | func (a ByCommand) Len() int { return len(a) } 92 | func (a ByCommand) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 93 | func (a ByCommand) Less(i, j int) bool { return a[i].Command > a[j].Command } 94 | 95 | type ByDescription []SnippetInfo 96 | 97 | func (a ByDescription) Len() int { return len(a) } 98 | func (a ByDescription) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 99 | func (a ByDescription) Less(i, j int) bool { return a[i].Description > a[j].Description } 100 | 101 | type ByOutput []SnippetInfo 102 | 103 | func (a ByOutput) Len() int { return len(a) } 104 | func (a ByOutput) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 105 | func (a ByOutput) Less(i, j int) bool { return a[i].Output > a[j].Output } 106 | -------------------------------------------------------------------------------- /cmd/new.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/chzyer/readline" 11 | "github.com/fatih/color" 12 | "github.com/knqyf263/pet/config" 13 | "github.com/knqyf263/pet/snippet" 14 | petSync "github.com/knqyf263/pet/sync" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | // newCmd represents the new command 19 | var newCmd = &cobra.Command{ 20 | Use: "new COMMAND", 21 | Short: "Create a new snippet", 22 | Long: `Create a new snippet (default: $HOME/.config/pet/snippet.toml)`, 23 | RunE: new, 24 | } 25 | 26 | func CanceledError() error { 27 | return errors.New("canceled") 28 | } 29 | 30 | func scan(message string, out io.Writer, in io.ReadCloser, allowEmpty bool) (string, error) { 31 | f, err := os.CreateTemp("", "pet-") 32 | if err != nil { 33 | return "", err 34 | } 35 | defer os.Remove(f.Name()) // clean up temp file 36 | tempFile := f.Name() 37 | 38 | l, err := readline.NewEx(&readline.Config{ 39 | Stdout: out, 40 | Stdin: in, 41 | Prompt: message, 42 | HistoryFile: tempFile, 43 | InterruptPrompt: "^C", 44 | EOFPrompt: "exit", 45 | 46 | HistorySearchFold: true, 47 | }) 48 | 49 | if err != nil { 50 | return "", err 51 | } 52 | defer l.Close() 53 | 54 | for { 55 | line, err := l.Readline() 56 | if err == readline.ErrInterrupt { 57 | if len(line) == 0 { 58 | break 59 | } else { 60 | continue 61 | } 62 | } else if err == io.EOF { 63 | break 64 | } 65 | 66 | // If empty string, just ignore tags 67 | line = strings.TrimSpace(line) 68 | if line == "" && !allowEmpty { 69 | continue 70 | } else if line == "" { 71 | return "", nil 72 | } 73 | return line, nil 74 | } 75 | return "", CanceledError() 76 | } 77 | 78 | func new(cmd *cobra.Command, args []string) (err error) { 79 | var command string 80 | var description string 81 | var tags []string 82 | 83 | var snippets snippet.Snippets 84 | if err := snippets.Load(); err != nil { 85 | return err 86 | } 87 | 88 | if len(args) > 0 { 89 | command = strings.Join(args, " ") 90 | fmt.Fprintf(color.Output, "%s %s\n", color.HiYellowString("Command>"), command) 91 | } else { 92 | command, err = scan(color.HiYellowString("Command> "), os.Stdout, os.Stdin, false) 93 | if err != nil { 94 | return err 95 | } 96 | } 97 | description, err = scan(color.HiGreenString("Description> "), os.Stdout, os.Stdin, false) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | if config.Flag.Tag { 103 | var t string 104 | if t, err = scan(color.HiCyanString("Tag> "), os.Stdout, os.Stdin, true); err != nil { 105 | return err 106 | } 107 | 108 | if t != "" { 109 | tags = strings.Fields(t) 110 | } 111 | } 112 | 113 | for _, s := range snippets.Snippets { 114 | if s.Description == description { 115 | return fmt.Errorf("snippet [%s] already exists", description) 116 | } 117 | } 118 | 119 | newSnippet := snippet.SnippetInfo{ 120 | Description: description, 121 | Command: command, 122 | Tag: tags, 123 | } 124 | snippets.Snippets = append(snippets.Snippets, newSnippet) 125 | if err = snippets.Save(); err != nil { 126 | return err 127 | } 128 | 129 | snippetFile := config.Conf.General.SnippetFile 130 | if config.Conf.Gist.AutoSync { 131 | return petSync.AutoSync(snippetFile) 132 | } 133 | 134 | return nil 135 | } 136 | 137 | func init() { 138 | RootCmd.AddCommand(newCmd) 139 | newCmd.Flags().BoolVarP(&config.Flag.Tag, "tag", "t", false, 140 | `Display tag prompt (delimiter: space)`) 141 | } 142 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | 20 | jobs: 21 | analyze: 22 | name: Analyze 23 | # Runner size impacts CodeQL analysis time. To learn more, please see: 24 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 25 | # - https://gh.io/supported-runners-and-hardware-resources 26 | # - https://gh.io/using-larger-runners 27 | # Consider using larger runners for possible analysis time improvements. 28 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 29 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 30 | permissions: 31 | # required for all workflows 32 | security-events: write 33 | 34 | # only required for workflows in private repositories 35 | actions: read 36 | contents: read 37 | 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | language: [ 'go' ] 42 | # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] 43 | # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both 44 | # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 45 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 46 | 47 | steps: 48 | - name: Checkout repository 49 | uses: actions/checkout@v4 50 | 51 | # Initializes the CodeQL tools for scanning. 52 | - name: Initialize CodeQL 53 | uses: github/codeql-action/init@v3 54 | with: 55 | languages: ${{ matrix.language }} 56 | # If you wish to specify custom queries, you can do so here or in a config file. 57 | # By default, queries listed here will override any specified in a config file. 58 | # Prefix the list here with "+" to use these queries and those in the config file. 59 | 60 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 61 | # queries: security-extended,security-and-quality 62 | 63 | 64 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 65 | # If this step fails, then you should remove it and run the build manually (see below) 66 | - name: Autobuild 67 | uses: github/codeql-action/autobuild@v3 68 | 69 | # ℹ️ Command-line programs to run using the OS shell. 70 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 71 | 72 | # If the Autobuild fails above, remove it and uncomment the following three lines. 73 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 74 | 75 | # - run: | 76 | # echo "Run, Build Application using script" 77 | # ./location_of_script_within_repo/buildscript.sh 78 | 79 | - name: Perform CodeQL Analysis 80 | uses: github/codeql-action/analyze@v3 81 | with: 82 | category: "/language:${{matrix.language}}" 83 | -------------------------------------------------------------------------------- /sync/gist.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "github.com/briandowns/spinner" 10 | "github.com/google/go-github/github" 11 | "github.com/knqyf263/pet/config" 12 | "github.com/pkg/errors" 13 | "golang.org/x/oauth2" 14 | ) 15 | 16 | const ( 17 | githubTokenEnvVariable = "PET_GITHUB_ACCESS_TOKEN" 18 | ) 19 | 20 | // GistClient manages communication with Gist 21 | type GistClient struct { 22 | Client *github.Client 23 | ID string 24 | } 25 | 26 | // NewGistClient returns GistClient 27 | func NewGistClient() (Client, error) { 28 | accessToken, err := getGithubAccessToken() 29 | if err != nil { 30 | return nil, fmt.Errorf(`access_token is empty. 31 | Go https://github.com/settings/tokens/new and create access_token (only need "gist" scope). 32 | Write access_token in config file (pet configure) or export $%v. 33 | `, githubTokenEnvVariable) 34 | } 35 | 36 | client := GistClient{ 37 | Client: githubClient(accessToken), 38 | ID: config.Conf.Gist.GistID, 39 | } 40 | return client, nil 41 | } 42 | 43 | func getGithubAccessToken() (string, error) { 44 | if config.Conf.Gist.AccessToken != "" { 45 | return config.Conf.Gist.AccessToken, nil 46 | } else if os.Getenv(githubTokenEnvVariable) != "" { 47 | return os.Getenv(githubTokenEnvVariable), nil 48 | } 49 | return "", errors.New("Github AccessToken not found in any source") 50 | } 51 | 52 | // GetSnippet returns the remote snippet 53 | func (g GistClient) GetSnippet() (*Snippet, error) { 54 | s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) 55 | s.Start() 56 | s.Suffix = " Getting Gist..." 57 | defer s.Stop() 58 | 59 | if g.ID == "" { 60 | return &Snippet{}, nil 61 | } 62 | 63 | gist, res, err := g.Client.Gists.Get(context.Background(), g.ID) 64 | if err != nil { 65 | if res != nil && res.StatusCode == 404 { 66 | return nil, errors.Wrapf(err, "No gist ID (%s)", g.ID) 67 | } 68 | return nil, errors.Wrapf(err, "Failed to get gist") 69 | } 70 | 71 | content := "" 72 | filename := config.Conf.Gist.FileName 73 | for _, file := range gist.Files { 74 | if *file.Filename == filename { 75 | content = *file.Content 76 | } 77 | } 78 | if content == "" { 79 | return nil, fmt.Errorf("%s is empty", filename) 80 | } 81 | 82 | return &Snippet{ 83 | Content: content, 84 | UpdatedAt: *gist.UpdatedAt, 85 | }, nil 86 | } 87 | 88 | // UploadSnippet uploads local snippets to Gist 89 | func (g GistClient) UploadSnippet(content string) error { 90 | gist := &github.Gist{ 91 | Description: github.String("description"), 92 | Public: github.Bool(config.Conf.Gist.Public), 93 | Files: map[github.GistFilename]github.GistFile{ 94 | github.GistFilename(config.Conf.Gist.FileName): github.GistFile{ 95 | Content: github.String(content), 96 | }, 97 | }, 98 | } 99 | 100 | if g.ID == "" { 101 | gistID, err := g.createGist(context.Background(), gist) 102 | if err != nil { 103 | return err 104 | } 105 | fmt.Printf("Gist ID: %s\n", *gistID) 106 | } else { 107 | if err := g.updateGist(context.Background(), gist); err != nil { 108 | return errors.Wrap(err, "Failed to update gist") 109 | } 110 | } 111 | return nil 112 | } 113 | 114 | func (g GistClient) createGist(ctx context.Context, gist *github.Gist) (gistID *string, err error) { 115 | s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) 116 | s.Start() 117 | s.Suffix = " Creating Gist..." 118 | defer s.Stop() 119 | 120 | retGist, _, err := g.Client.Gists.Create(ctx, gist) 121 | if err != nil { 122 | return nil, errors.Wrap(err, "Failed to create gist") 123 | } 124 | return retGist.ID, nil 125 | } 126 | 127 | func (g GistClient) updateGist(ctx context.Context, gist *github.Gist) (err error) { 128 | s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) 129 | s.Start() 130 | s.Suffix = " Updating Gist..." 131 | defer s.Stop() 132 | 133 | if _, _, err = g.Client.Gists.Edit(ctx, g.ID, gist); err != nil { 134 | return errors.Wrap(err, "Failed to edit gist") 135 | } 136 | return nil 137 | } 138 | 139 | func githubClient(accessToken string) *github.Client { 140 | ts := oauth2.StaticTokenSource( 141 | &oauth2.Token{AccessToken: accessToken}, 142 | ) 143 | tc := oauth2.NewClient(oauth2.NoContext, ts) 144 | client := github.NewClient(tc) 145 | return client 146 | } 147 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "runtime" 9 | 10 | "github.com/BurntSushi/toml" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // Conf is global config variable 15 | var Conf Config 16 | 17 | // Config is a struct of config 18 | type Config struct { 19 | General GeneralConfig `toml:"General"` 20 | Gist GistConfig `toml:"Gist"` 21 | GitLab GitLabConfig `toml:"GitLab"` 22 | } 23 | 24 | // GeneralConfig is a struct of general config 25 | type GeneralConfig struct { 26 | SnippetFile string `toml:"snippetfile"` 27 | Editor string `toml:"editor"` 28 | Column int `toml:"column"` 29 | SelectCmd string `toml:"selectcmd"` 30 | Backend string `toml:"backend"` 31 | SortBy string `toml:"sortby"` 32 | Cmd []string `toml:"cmd"` 33 | } 34 | 35 | // GistConfig is a struct of config for Gist 36 | type GistConfig struct { 37 | FileName string `toml:"file_name"` 38 | AccessToken string `toml:"access_token"` 39 | GistID string `toml:"gist_id"` 40 | Public bool `toml:"public"` 41 | AutoSync bool `toml:"auto_sync"` 42 | } 43 | 44 | // GitLabConfig is a struct of config for GitLabSnippet 45 | type GitLabConfig struct { 46 | FileName string `toml:"file_name"` 47 | AccessToken string `toml:"access_token"` 48 | Url string `toml:"url"` 49 | ID string `toml:"id"` 50 | Visibility string `toml:"visibility"` 51 | AutoSync bool `toml:"auto_sync"` 52 | Insecure bool `toml:"skip_ssl"` 53 | } 54 | 55 | // Flag is global flag variable 56 | var Flag FlagConfig 57 | 58 | // FlagConfig is a struct of flag 59 | type FlagConfig struct { 60 | Debug bool 61 | Query string 62 | FilterTag string 63 | Command bool 64 | Delimiter string 65 | OneLine bool 66 | Color bool 67 | Tag bool 68 | } 69 | 70 | // Load loads a config toml 71 | func (cfg *Config) Load(file string) error { 72 | _, err := os.Stat(file) 73 | if err == nil { 74 | _, err := toml.DecodeFile(file, cfg) 75 | if err != nil { 76 | return err 77 | } 78 | cfg.General.SnippetFile = expandPath(cfg.General.SnippetFile) 79 | return nil 80 | } 81 | 82 | if !os.IsNotExist(err) { 83 | return err 84 | } 85 | f, err := os.Create(file) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | dir, err := GetDefaultConfigDir() 91 | if err != nil { 92 | return errors.Wrap(err, "Failed to get the default config directory") 93 | } 94 | cfg.General.SnippetFile = filepath.Join(dir, "snippet.toml") 95 | _, err = os.Create(cfg.General.SnippetFile) 96 | if err != nil { 97 | return errors.Wrap(err, "Failed to create a config file") 98 | } 99 | 100 | cfg.General.Editor = os.Getenv("EDITOR") 101 | if cfg.General.Editor == "" && runtime.GOOS != "windows" { 102 | if isCommandAvailable("sensible-editor") { 103 | cfg.General.Editor = "sensible-editor" 104 | } else { 105 | cfg.General.Editor = "vim" 106 | } 107 | } 108 | cfg.General.Column = 40 109 | cfg.General.SelectCmd = "fzf --ansi --layout=reverse --border --height=90% --pointer=* --cycle --prompt=Snippets:" 110 | cfg.General.Backend = "gist" 111 | 112 | cfg.Gist.FileName = "pet-snippet.toml" 113 | 114 | cfg.GitLab.FileName = "pet-snippet.toml" 115 | cfg.GitLab.Visibility = "private" 116 | 117 | return toml.NewEncoder(f).Encode(cfg) 118 | } 119 | 120 | // GetDefaultConfigDir returns the default config directory 121 | func GetDefaultConfigDir() (dir string, err error) { 122 | if env, ok := os.LookupEnv("PET_CONFIG_DIR"); ok { 123 | dir = env 124 | } else if runtime.GOOS == "windows" { 125 | dir = os.Getenv("APPDATA") 126 | if dir == "" { 127 | dir = filepath.Join(os.Getenv("USERPROFILE"), "Application Data", "pet") 128 | } 129 | dir = filepath.Join(dir, "pet") 130 | } else { 131 | dir = filepath.Join(os.Getenv("HOME"), ".config", "pet") 132 | } 133 | if err := os.MkdirAll(dir, 0o700); err != nil { 134 | return "", fmt.Errorf("cannot create directory: %v", err) 135 | } 136 | return dir, nil 137 | } 138 | 139 | func expandPath(s string) string { 140 | if len(s) >= 2 && s[0] == '~' && os.IsPathSeparator(s[1]) { 141 | if runtime.GOOS == "windows" { 142 | s = filepath.Join(os.Getenv("USERPROFILE"), s[2:]) 143 | } else { 144 | s = filepath.Join(os.Getenv("HOME"), s[2:]) 145 | } 146 | } 147 | return os.Expand(s, os.Getenv) 148 | } 149 | 150 | func isCommandAvailable(name string) bool { 151 | cmd := exec.Command("/bin/sh", "-c", "command -v "+name) 152 | if err := cmd.Run(); err != nil { 153 | return false 154 | } 155 | return true 156 | } 157 | -------------------------------------------------------------------------------- /sync/gitlab.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "net/http" 8 | "crypto/tls" 9 | "strconv" 10 | "time" 11 | 12 | "github.com/briandowns/spinner" 13 | "github.com/knqyf263/pet/config" 14 | "github.com/pkg/errors" 15 | "github.com/xanzy/go-gitlab" 16 | ) 17 | 18 | const ( 19 | gitlabTokenEnvVariable = "PET_GITLAB_ACCESS_TOKEN" 20 | ) 21 | 22 | // GitLabClient manages communication with GitLab Snippets 23 | type GitLabClient struct { 24 | Client *gitlab.Client 25 | ID int 26 | } 27 | 28 | // NewGitLabClient returns GitLabClient 29 | func NewGitLabClient() (Client, error) { 30 | accessToken, err := getGitlabAccessToken() 31 | if err != nil { 32 | return nil, fmt.Errorf(`access_token is empty. 33 | Go https://gitlab.com/profile/personal_access_tokens and create access_token. 34 | Write access_token in config file (pet configure) or export $%v. 35 | `, gitlabTokenEnvVariable) 36 | } 37 | 38 | u := "https://git.mydomain.com/api/v4" 39 | id := 0 40 | 41 | h := &http.Client{} 42 | 43 | if config.Conf.GitLab.Url != "" { 44 | fmt.Println(config.Conf.GitLab.Url) 45 | u = config.Conf.GitLab.Url 46 | } 47 | 48 | if config.Conf.GitLab.Insecure == true { 49 | tr := &http.Transport{ 50 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 51 | } 52 | h = &http.Client{Transport: tr} 53 | } 54 | 55 | c, err := gitlab.NewClient(accessToken, gitlab.WithBaseURL(u), gitlab.WithHTTPClient(h)) 56 | if err != nil { 57 | return nil, errors.Wrapf(err, "Failed to create GitLab client: %d", id) 58 | } 59 | 60 | if config.Conf.GitLab.ID == "" { 61 | client := GitLabClient{ 62 | Client: c, 63 | ID: id, 64 | } 65 | 66 | return client, nil 67 | } 68 | 69 | id, err = strconv.Atoi(config.Conf.GitLab.ID) 70 | if err != nil { 71 | return nil, errors.Wrapf(err, "Invalid GitLab Snippet ID: %d", id) 72 | } 73 | 74 | client := GitLabClient{ 75 | Client: c, 76 | ID: id, 77 | } 78 | 79 | return client, nil 80 | } 81 | 82 | func getGitlabAccessToken() (string, error) { 83 | if config.Conf.GitLab.AccessToken != "" { 84 | return config.Conf.GitLab.AccessToken, nil 85 | } else if os.Getenv(gitlabTokenEnvVariable) != "" { 86 | return os.Getenv(gitlabTokenEnvVariable), nil 87 | } 88 | return "", errors.New("GitLab AccessToken not found in any source") 89 | } 90 | 91 | // GetSnippet returns the remote snippet 92 | func (g GitLabClient) GetSnippet() (*Snippet, error) { 93 | s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) 94 | s.Start() 95 | s.Suffix = " Getting GitLab Snippet..." 96 | defer s.Stop() 97 | 98 | if g.ID == 0 { 99 | return &Snippet{}, nil 100 | } 101 | 102 | snippet, res, err := g.Client.Snippets.GetSnippet(g.ID) 103 | if err != nil { 104 | if res.StatusCode == 404 { 105 | return nil, errors.Wrapf(err, "No GitLab Snippet ID (%d)", g.ID) 106 | } 107 | return nil, errors.Wrapf(err, "Failed to get GitLab Snippet (ID: %d)", g.ID) 108 | } 109 | 110 | filename := config.Conf.GitLab.FileName 111 | if snippet.FileName != filename { 112 | return nil, fmt.Errorf("No snippet file in GitLab Snippet (ID: %d)", g.ID) 113 | } 114 | 115 | contentByte, _, err := g.Client.Snippets.SnippetContent(g.ID) 116 | if err != nil { 117 | return nil, errors.Wrapf(err, "Failed to get GitLab Snippet content (ID: %d)", g.ID) 118 | } 119 | 120 | content := string(contentByte) 121 | if content == "" { 122 | return nil, fmt.Errorf("%s is empty", filename) 123 | } 124 | 125 | return &Snippet{ 126 | Content: content, 127 | UpdatedAt: *snippet.UpdatedAt, 128 | }, nil 129 | } 130 | 131 | // UploadSnippet uploads local snippets to GitLab Snippet 132 | func (g GitLabClient) UploadSnippet(content string) error { 133 | if g.ID == 0 { 134 | id, err := g.createSnippet(context.Background(), content) 135 | if err != nil { 136 | return errors.Wrap(err, "Failed to create GitLab Snippet") 137 | } 138 | fmt.Printf("GitLab Snippet ID: %d\n", id) 139 | } else { 140 | if err := g.updateSnippet(context.Background(), content); err != nil { 141 | return errors.Wrap(err, "Failed to update GitLab Snippet") 142 | } 143 | } 144 | return nil 145 | } 146 | 147 | func (g GitLabClient) createSnippet(ctx context.Context, content string) (id int, err error) { 148 | s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) 149 | s.Start() 150 | s.Suffix = " Creating GitLab Snippet..." 151 | defer s.Stop() 152 | 153 | opt := &gitlab.CreateSnippetOptions{ 154 | Title: gitlab.String("pet-snippet"), 155 | FileName: gitlab.String(config.Conf.GitLab.FileName), 156 | Description: gitlab.String("Snippet file generated by pet"), 157 | Content: gitlab.String(content), 158 | Visibility: gitlab.Visibility(gitlab.VisibilityValue(config.Conf.GitLab.Visibility)), 159 | } 160 | 161 | ret, _, err := g.Client.Snippets.CreateSnippet(opt) 162 | if err != nil { 163 | return -1, errors.Wrap(err, "Failed to create GitLab Snippet") 164 | } 165 | return ret.ID, nil 166 | } 167 | 168 | func (g GitLabClient) updateSnippet(ctx context.Context, content string) (err error) { 169 | s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) 170 | s.Start() 171 | s.Suffix = " Updating GitLab Snippet..." 172 | defer s.Stop() 173 | 174 | opt := &gitlab.UpdateSnippetOptions{ 175 | Title: gitlab.String("pet-snippet"), 176 | FileName: gitlab.String(config.Conf.GitLab.FileName), 177 | Description: gitlab.String("Snippet file generated by pet"), 178 | Content: gitlab.String(content), 179 | Visibility: gitlab.Visibility(gitlab.VisibilityValue(config.Conf.GitLab.Visibility)), 180 | } 181 | 182 | _, _, err = g.Client.Snippets.UpdateSnippet(g.ID, opt) 183 | if err != nil { 184 | return errors.Wrap(err, "Failed to update GitLab Snippet") 185 | } 186 | return nil 187 | } 188 | -------------------------------------------------------------------------------- /dialog/view.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "regexp" 7 | 8 | "github.com/awesome-gocui/gocui" 9 | ) 10 | 11 | var ( 12 | layoutStep = 3 13 | curView = -1 14 | 15 | // This is for matching multiple default values in parameters 16 | parameterMultipleValueRegex = `(\|_.*?_\|)` 17 | ) 18 | 19 | // createView sets up a new view with the given parameters. 20 | func createView(g *gocui.Gui, name string, coords []int, editable bool) (*gocui.View, error) { 21 | if StringInSlice(name, views) { 22 | return nil, nil 23 | } 24 | 25 | v, err := g.SetView(name, coords[0], coords[1], coords[2], coords[3], 0) 26 | if err != nil && err != gocui.ErrUnknownView { 27 | return nil, err 28 | } 29 | 30 | v.Title = name 31 | v.Wrap = true 32 | v.Autoscroll = true 33 | v.Editable = editable 34 | 35 | views = append(views, name) 36 | 37 | return v, nil 38 | } 39 | 40 | func generateSingleParameterView(g *gocui.Gui, name string, defaultParam string, coords []int, editable bool) error { 41 | view, err := createView(g, name, coords, editable) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | g.SetKeybinding(view.Name(), gocui.KeyCtrlK, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error { 47 | v.Clear() 48 | return nil 49 | }) 50 | 51 | fmt.Fprint(view, defaultParam) 52 | return nil 53 | } 54 | 55 | func generateMultipleParameterView(g *gocui.Gui, name string, defaultParams []string, coords []int, editable bool) error { 56 | view, err := createView(g, name, coords, editable) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | currentOpt := 0 62 | maxOpt := len(defaultParams) 63 | 64 | fmt.Fprint(view, defaultParams[currentOpt]) 65 | 66 | viewTitle := name 67 | // Adjust view title to hint the user about the available 68 | // options if there are more than one 69 | if maxOpt > 1 { 70 | viewTitle = name + " (UP/DOWN => Select default value)" 71 | } 72 | 73 | view.Title = viewTitle 74 | 75 | g.SetKeybinding(view.Name(), gocui.KeyArrowDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error { 76 | if maxOpt == 0 { 77 | return nil 78 | } 79 | next := currentOpt + 1 80 | if next >= maxOpt { 81 | next = 0 82 | } 83 | v.Clear() 84 | fmt.Fprint(v, defaultParams[next]) 85 | currentOpt = next 86 | return nil 87 | }) 88 | 89 | g.SetKeybinding(view.Name(), gocui.KeyArrowUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error { 90 | if maxOpt == 0 { 91 | return nil 92 | } 93 | prev := currentOpt - 1 94 | if prev < 0 { 95 | prev = maxOpt - 1 96 | } 97 | v.Clear() 98 | fmt.Fprint(v, defaultParams[prev]) 99 | currentOpt = prev 100 | return nil 101 | }) 102 | 103 | return nil 104 | } 105 | 106 | // GenerateParamsLayout generates CUI to receive params 107 | func GenerateParamsLayout(params [][2]string, command string) { 108 | g, err := gocui.NewGui(gocui.OutputNormal, false) 109 | if err != nil { 110 | log.Panicln(err) 111 | } 112 | defer g.Close() 113 | 114 | g.Highlight = true 115 | g.Cursor = true 116 | g.SelFgColor = gocui.ColorGreen 117 | 118 | g.SetManagerFunc(layout) 119 | 120 | maxX, maxY := g.Size() 121 | leftX := (maxX / 2) - (maxX / 3) 122 | rightX := (maxX / 2) + (maxX / 3) 123 | 124 | generateSingleParameterView(g, "Command(TAB => Select next, ENTER => Execute command):", 125 | command, []int{leftX, maxY / 10, rightX, maxY/10 + 5}, false) 126 | idx := 0 127 | 128 | // Create a view for each param 129 | for _, pair := range params { 130 | // Unpack parameter key and value 131 | parameterKey, parameterValue := pair[0], pair[1] 132 | 133 | // Check value for multiple defaults 134 | r := regexp.MustCompile(parameterMultipleValueRegex) 135 | matches := r.FindAllStringSubmatch(parameterValue, -1) 136 | 137 | if len(matches) > 0 { 138 | // Extract the default values and generate multiple params view 139 | parameters := []string{} 140 | for _, p := range matches { 141 | _, matchedGroup := p[0], p[1] 142 | // Remove the separators 143 | matchedGroup = matchedGroup[2 : len(matchedGroup)-2] 144 | parameters = append(parameters, matchedGroup) 145 | } 146 | generateMultipleParameterView( 147 | g, parameterKey, parameters, []int{ 148 | leftX, 149 | (maxY / 4) + (idx+1)*layoutStep, 150 | rightX, 151 | (maxY / 4) + 2 + (idx+1)*layoutStep}, 152 | true) 153 | } else { 154 | // Generate single param view using the single value 155 | generateSingleParameterView(g, parameterKey, parameterValue, 156 | []int{ 157 | leftX, 158 | (maxY / 4) + (idx+1)*layoutStep, 159 | rightX, 160 | (maxY / 4) + 2 + (idx+1)*layoutStep}, 161 | true) 162 | } 163 | idx++ 164 | } 165 | 166 | initKeybindings(g) 167 | 168 | curView = 0 169 | if idx > 0 { 170 | curView = 1 171 | } 172 | g.SetCurrentView(views[curView]) 173 | 174 | if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { 175 | log.Panicln(err) 176 | } 177 | } 178 | 179 | func nextView(g *gocui.Gui) error { 180 | next := curView + 1 181 | if next > len(views)-1 { 182 | next = 0 183 | } 184 | 185 | if _, err := g.SetCurrentView(views[next]); err != nil { 186 | return err 187 | } 188 | 189 | curView = next 190 | return nil 191 | } 192 | 193 | func initKeybindings(g *gocui.Gui) error { 194 | if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { 195 | return err 196 | } 197 | 198 | if err := g.SetKeybinding("", gocui.KeyEnter, gocui.ModNone, evaluateParams); err != nil { 199 | return err 200 | } 201 | if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, 202 | func(g *gocui.Gui, v *gocui.View) error { 203 | return nextView(g) 204 | }); err != nil { 205 | return err 206 | } 207 | return nil 208 | } 209 | 210 | func layout(g *gocui.Gui) error { 211 | return nil 212 | } 213 | 214 | func quit(_ *gocui.Gui, _ *gocui.View) error { 215 | return gocui.ErrQuit 216 | } 217 | -------------------------------------------------------------------------------- /dialog/params_test.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-test/deep" 7 | ) 8 | 9 | func TestSearchForParams(t *testing.T) { 10 | command := " hello" 11 | 12 | want := [][2]string{ 13 | {"a", "1"}, 14 | {"b", ""}, 15 | } 16 | 17 | got := SearchForParams(command) 18 | 19 | if diff := deep.Equal(want, got); diff != nil { 20 | t.Fatal(diff) 21 | } 22 | } 23 | 24 | func TestSearchForParams_WithNoParams(t *testing.T) { 25 | command := "no params" 26 | 27 | got := SearchForParams(command) 28 | 29 | if got != nil { 30 | t.Fatalf("wanted nil, got '%v'", got) 31 | } 32 | } 33 | 34 | func TestSearchForParams_WithMultipleParams(t *testing.T) { 35 | command := " " 36 | 37 | want := [][2]string{ 38 | {"a", "1"}, 39 | {"b", ""}, 40 | {"c", "3"}, 41 | } 42 | 43 | got := SearchForParams(command) 44 | 45 | if diff := deep.Equal(want, got); diff != nil { 46 | t.Fatal(diff) 47 | } 48 | } 49 | 50 | func TestSearchForParams_WithEmptyCommand(t *testing.T) { 51 | command := "" 52 | 53 | got := SearchForParams(command) 54 | 55 | if got != nil { 56 | t.Fatalf("wanted nil, got '%v'", got) 57 | } 58 | } 59 | 60 | func TestSearchForParams_WithNewline(t *testing.T) { 61 | command := " hello\n" 62 | 63 | want := [][2]string{ 64 | {"a", "1"}, 65 | {"b", ""}, 66 | {"c", "3"}, 67 | } 68 | 69 | got := SearchForParams(command) 70 | 71 | if diff := deep.Equal(want, got); diff != nil { 72 | t.Fatal(diff) 73 | } 74 | } 75 | 76 | func TestSearchForParams_ValueWithSpaces(t *testing.T) { 77 | command := "example_function --flag=" 78 | 79 | want := [][2]string{ 80 | {"param", "Lots of Bananas"}, 81 | } 82 | 83 | got := SearchForParams(command) 84 | 85 | if diff := deep.Equal(want, got); diff != nil { 86 | t.Fatal(diff) 87 | } 88 | } 89 | 90 | func TestSearchForParams_InvalidParamFormat(t *testing.T) { 91 | command := " hello" 92 | want := [][2]string{ 93 | {"b", ""}, 94 | } 95 | got := SearchForParams(command) 96 | 97 | if diff := deep.Equal(want, got); diff != nil { 98 | t.Fatal(diff) 99 | } 100 | } 101 | 102 | func TestSearchForParams_InvalidParamFormatWithoutSpaces(t *testing.T) { 103 | command := "hello" 104 | want := [][2]string{ 105 | {"b", ""}, 106 | } 107 | got := SearchForParams(command) 108 | 109 | if diff := deep.Equal(want, got); diff != nil { 110 | t.Fatal(diff) 111 | } 112 | } 113 | 114 | func TestSearchForParams_ConfusingBrackets(t *testing.T) { 115 | command := "cat < \nEOF" 116 | want := [][2]string{ 117 | {"file", "path/to/file"}, 118 | } 119 | got := SearchForParams(command) 120 | if diff := deep.Equal(want, got); diff != nil { 121 | t.Fatal(diff) 122 | } 123 | } 124 | 125 | func TestSearchForParams_MultipleParamsSameKey(t *testing.T) { 126 | command := " " 127 | want := [][2]string{ 128 | {"a", "3"}, 129 | } 130 | got := SearchForParams(command) 131 | 132 | if diff := deep.Equal(want, got); diff != nil { 133 | t.Fatal(diff) 134 | } 135 | } 136 | 137 | func TestSearchForParams_MultipleParamsSameKeyDifferentValues(t *testing.T) { 138 | command := " " 139 | want := [][2]string{ 140 | {"a", "3"}, 141 | } 142 | got := SearchForParams(command) 143 | 144 | if diff := deep.Equal(want, got); diff != nil { 145 | t.Fatal(diff) 146 | } 147 | } 148 | 149 | func TestSearchForParams_MultipleParamsSameKeyDifferentValues_MultipleLines(t *testing.T) { 150 | command := " \n" 151 | want := [][2]string{ 152 | {"a", "3"}, 153 | {"b", "4"}, 154 | } 155 | got := SearchForParams(command) 156 | 157 | if diff := deep.Equal(want, got); diff != nil { 158 | t.Fatal(diff) 159 | } 160 | } 161 | 162 | func TestSearchForParams_MultipleParamsSameKeyDifferentValues_InvalidFormat(t *testing.T) { 163 | command := " " 164 | want := [][2]string{ 165 | {"a", "3"}, 166 | } 167 | got := SearchForParams(command) 168 | 169 | if diff := deep.Equal(want, got); diff != nil { 170 | t.Fatal(diff) 171 | } 172 | } 173 | 174 | func TestSearchForParams_MultipleParamsSameKeyDifferentValues_InvalidFormat_MultipleLines(t *testing.T) { 175 | command := " " 176 | want := [][2]string{ 177 | {"a", "2"}, 178 | {"b", "4"}, 179 | } 180 | 181 | got := SearchForParams(command) 182 | 183 | if diff := deep.Equal(want, got); diff != nil { 184 | t.Fatal(diff) 185 | } 186 | } 187 | 188 | func TestSearchForParams_MultipleParamsSameKeyDifferentValues_InvalidFormat_MultipleLines2(t *testing.T) { 189 | command := " \n\"" 203 | want := [][2]string{ 204 | {"param", "Hello == World!==="}, 205 | } 206 | 207 | got := SearchForParams(command) 208 | 209 | if diff := deep.Equal(want, got); diff != nil { 210 | t.Fatal(diff) 211 | } 212 | } 213 | 214 | func TestSearchForParams_MultipleDefaultValuesDoNotBreakFunction(t *testing.T) { 215 | command := "echo \" , \"" 216 | want := [][2]string{ 217 | {"param", "|_Hello_||_Hello world_||_How are you?_|"}, 218 | {"second", "Hello"}, 219 | {"third", ""}, 220 | } 221 | 222 | got := SearchForParams(command) 223 | 224 | if diff := deep.Equal(want, got); diff != nil { 225 | t.Fatal(diff) 226 | } 227 | } 228 | 229 | func TestInsertParams(t *testing.T) { 230 | command := " hello" 231 | 232 | params := map[string]string{ 233 | "a": "test", 234 | "b": "case", 235 | } 236 | 237 | got := insertParams(command, params) 238 | want := "test test case hello" 239 | if want != got { 240 | t.Fatalf("wanted '%s', got '%s'", want, got) 241 | } 242 | } 243 | 244 | func TestInsertParams_unique_parameters(t *testing.T) { 245 | command := "curl -X POST \"/\" -H 'Content-Type: application/json'" 246 | 247 | params := map[string]string{ 248 | "host": "localhost:9200", 249 | "index": "test", 250 | } 251 | 252 | got := insertParams(command, params) 253 | want := "curl -X POST \"localhost:9200/test\" -H 'Content-Type: application/json'" 254 | if got != want { 255 | t.Fatalf("got %s, want %s", got, want) 256 | } 257 | } 258 | 259 | func TestInsertParams_complex(t *testing.T) { 260 | command := "something //_delete_by_query/" 261 | 262 | params := map[string]string{ 263 | "host": "localhost:9200", 264 | "test": "case", 265 | } 266 | 267 | got := insertParams(command, params) 268 | want := "something localhost:9200/case/_delete_by_query/localhost:9200" 269 | if got != want { 270 | t.Fatalf("got %s, want %s", got, want) 271 | } 272 | } 273 | 274 | func TestInsertParams_EqualsInDefaultValueIgnored(t *testing.T) { 275 | command := "echo \"\"" 276 | 277 | params := map[string]string{ 278 | "param": "something == something", 279 | } 280 | 281 | got := insertParams(command, params) 282 | want := "echo \"something == something\"" 283 | if got != want { 284 | t.Fatalf("got %s, want %s", got, want) 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY= 2 | github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= 4 | github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= 5 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 6 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 7 | github.com/awesome-gocui/gocui v1.1.0 h1:db2j7yFEoHZjpQFeE2xqiatS8bm1lO3THeLwE6MzOII= 8 | github.com/awesome-gocui/gocui v1.1.0/go.mod h1:M2BXkrp7PR97CKnPRT7Rk0+rtswChPtksw/vRAESGpg= 9 | github.com/briandowns/spinner v0.0.0-20170614154858-48dbb65d7bd5 h1:osZyZB7J4kE1tKLeaUjV6+uZVBfS835T0I/RxmwWw1w= 10 | github.com/briandowns/spinner v0.0.0-20170614154858-48dbb65d7bd5/go.mod h1:hw/JEQBIE+c/BLI4aKM8UU8v+ZqrD3h7HC27kKt8JQU= 11 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 12 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 13 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 14 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 15 | github.com/chzyer/test v0.0.0-20210722231415-061457976a23 h1:dZ0/VyGgQdVGAss6Ju0dt5P0QltE0SFY5Woh6hbIfiQ= 16 | github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 21 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 22 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 23 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 24 | github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM= 25 | github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= 26 | github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= 27 | github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 28 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 29 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 30 | github.com/google/go-github v15.0.0+incompatible h1:jlPg2Cpsxb/FyEV/MFiIE9tW/2RAevQNZDPeHbf5a94= 31 | github.com/google/go-github v15.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 32 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 33 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 34 | github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= 35 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 36 | github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= 37 | github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 38 | github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs= 39 | github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= 40 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 41 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 42 | github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= 43 | github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 44 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 45 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 46 | github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= 47 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 48 | github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= 49 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 50 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 51 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= 55 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 56 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 57 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 58 | github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= 59 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 60 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 61 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 62 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 63 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 64 | github.com/xanzy/go-gitlab v0.50.3 h1:M7ncgNhCN4jaFNyXxarJhCLa9Qi6fdmCxFFhMTQPZiY= 65 | github.com/xanzy/go-gitlab v0.50.3/go.mod h1:Q+hQhV508bDPoBijv7YjK/Lvlb4PhVhJdKqXVQrUoAE= 66 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 67 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 68 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 69 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 70 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 71 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 72 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 73 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 74 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 75 | golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 h1:JIqe8uIcRBHXDQVvZtHwp80ai3Lw3IJAeJEs55Dc1W0= 76 | golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 77 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 78 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 79 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 80 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 81 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 82 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 83 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 84 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 85 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 86 | golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 87 | golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= 88 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 89 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 90 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 91 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 92 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 93 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= 94 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 95 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 96 | google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk= 97 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 98 | gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61 h1:8ajkpB4hXVftY5ko905id+dOnmorcS2CHNxxHLLDcFM= 99 | gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61/go.mod h1:IfMagxm39Ys4ybJrDb7W3Ob8RwxftP0Yy+or/NVz1O8= 100 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 101 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 102 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pet : CLI Snippet Manager 2 | 3 | [![GitHub release](https://img.shields.io/github/release/knqyf263/pet.svg)](https://github.com/knqyf263/pet/releases/latest) 4 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/knqyf263/pet/blob/master/LICENSE) 5 | 6 | 7 | 8 | Simple command-line snippet manager, written in Go 9 | 10 | 11 | 12 | You can use variables (`` or `` ) in snippets. 13 | 14 | 15 | 16 | 17 | # Abstract 18 | 19 | `pet` is written in Go, and therefore you can just grab the binary releases and drop it in your $PATH. 20 | 21 | `pet` is a simple command-line snippet manager (inspired by [memo](https://github.com/mattn/memo)). 22 | I always forget commands that I rarely use. Moreover, it is difficult to search them from shell history. There are many similar commands, but they are all different. 23 | 24 | e.g. 25 | - `$ awk -F, 'NR <=2 {print $0}; NR >= 5 && NR <= 10 {print $0}' company.csv` (What I am looking for) 26 | - `$ awk -F, '$0 !~ "DNS|Protocol" {print $0}' packet.csv` 27 | - `$ awk -F, '{print $0} {if((NR-1) % 5 == 0) {print "----------"}}' test.csv` 28 | 29 | In the above case, I search by `awk` from shell history, but many commands hit. 30 | 31 | Even if I register an alias, I forget the name of alias (because I rarely use that command). 32 | 33 | So I made it possible to register snippets with description and search them easily. 34 | 35 | # TOC 36 | 37 | - [Main features](#main-features) 38 | - [Parameters] (#parameters) 39 | - [Examples](#examples) 40 | - [Register the previous command easily](#register-the-previous-command-easily) 41 | - [bash](#bash-prev-function) 42 | - [zsh](#zsh-prev-function) 43 | - [fish](#fish) 44 | - [Select snippets at the current line (like C-r) (RECOMMENDED)](#select-snippets-at-the-current-line-like-c-r-recommended) 45 | - [bash](#bash) 46 | - [zsh](#zsh) 47 | - [fish](#fish-1) 48 | - [Copy snippets to clipboard](#copy-snippets-to-clipboard) 49 | - [Features](#features) 50 | - [Edit snippets](#edit-snippets) 51 | - [Sync snippets](#sync-snippets) 52 | - [Hands-on Tutorial](#hands-on-tutorial) 53 | - [Usage](#usage) 54 | - [Snippet](#snippet) 55 | - [Configuration](#configuration) 56 | - [Selector option](#selector-option) 57 | - [Tag](#tag) 58 | - [Sync](#sync) 59 | - [Auto Sync](#auto-sync) 60 | - [Installation](#installation) 61 | - [Binary](#binary) 62 | - [Mac OS X / Homebrew](#mac-os-x--homebrew) 63 | - [RedHat, CentOS](#redhat-centos) 64 | - [Debian, Ubuntu](#debian-ubuntu) 65 | - [Archlinux](#archlinux) 66 | - [Build](#build) 67 | - [Migration](#migration) 68 | - [Contribute](#contribute) 69 | 70 | # Main features 71 | `pet` has the following features. 72 | 73 | - Register your command snippets easily. 74 | - Use variables (with one or several default values) in snippets. 75 | - Search snippets interactively. 76 | - Run snippets directly. 77 | - Edit snippets easily (config is just a TOML file). 78 | - Sync snippets via Gist or GitLab Snippets automatically. 79 | 80 | # Parameters 81 | There are `` ways of entering parameters. 82 | 83 | They can contain default values: Hello `` 84 | defined by the equal sign. 85 | 86 | They can even contain `` where the default value would be \spaces & = signs\>. 87 | 88 | Default values just can't \. 89 | 90 | They can also contain multiple default values: 91 | Hello `` 92 | 93 | The values in this case would be :Hello \John\_\|\|\_Sam\_\|\|\_Jane Doe = special #chars\_\|\> 94 | 95 | # Examples 96 | Some examples are shown below. 97 | 98 | ## Register the previous command easily 99 | By adding the following config to `.bashrc` or `.zshrc`, you can easily register the previous command. 100 | 101 | ### bash prev function 102 | 103 | ``` 104 | function prev() { 105 | PREV=$(echo `history | tail -n2 | head -n1` | sed 's/[0-9]* //') 106 | sh -c "pet new `printf %q "$PREV"`" 107 | } 108 | ``` 109 | 110 | ### zsh prev function 111 | 112 | ``` 113 | $ cat .zshrc 114 | function prev() { 115 | PREV=$(fc -lrn | head -n 1) 116 | sh -c "pet new `printf %q "$PREV"`" 117 | } 118 | ``` 119 | 120 | ### fish 121 | See below for details. 122 | https://github.com/otms61/fish-pet 123 | 124 | 125 | 126 | ## Select snippets at the current line (like C-r) (RECOMMENDED) 127 | 128 | ### bash 129 | By adding the following config to `.bashrc`, you can search snippets and output on the shell. 130 | This will also allow you to execute the commands yourself, which will add them to your shell history! This is basically the only way we can manipulate shell history. 131 | This also allows you to *chain* commands! [Example here](https://github.com/knqyf263/pet/discussions/266) 132 | 133 | ``` 134 | $ cat .bashrc 135 | function pet-select() { 136 | BUFFER=$(pet search --query "$READLINE_LINE") 137 | READLINE_LINE=$BUFFER 138 | READLINE_POINT=${#BUFFER} 139 | } 140 | bind -x '"\C-x\C-r": pet-select' 141 | ``` 142 | 143 | ### zsh 144 | 145 | ``` 146 | $ cat .zshrc 147 | function pet-select() { 148 | BUFFER=$(pet search --query "$LBUFFER") 149 | CURSOR=$#BUFFER 150 | zle redisplay 151 | } 152 | zle -N pet-select 153 | stty -ixon 154 | bindkey '^s' pet-select 155 | ``` 156 | 157 | ### fish 158 | See below for details. 159 | https://github.com/otms61/fish-pet 160 | 161 | 162 | 163 | 164 | ## Copy snippets to clipboard 165 | By using `pbcopy` on OS X, you can copy snippets to clipboard. 166 | 167 | 168 | 169 | # Features 170 | 171 | ## Edit snippets 172 | The snippets are managed in the TOML file, so it's easy to edit. 173 | 174 | 175 | 176 | 177 | ## Sync snippets 178 | You can share snippets via Gist. 179 | 180 | 181 | 182 | # Hands-on Tutorial 183 | 184 | To experience `pet` in action, try it out in this free O'Reilly Katacoda scenario, [Pet, a CLI Snippet Manager](https://katacoda.com/javajon/courses/kubernetes-tools/snippets-pet). As an example, you'll see how `pet` may enhance your productivity with the Kubernetes `kubectl` tool. Explore how you can use `pet` to curated a library of helpful snippets from the 800+ command variations with `kubectl`. 185 | 186 | # Usage 187 | 188 | ``` 189 | pet - Simple command-line snippet manager. 190 | 191 | Usage: 192 | pet [command] 193 | 194 | Available Commands: 195 | configure Edit config file 196 | edit Edit snippet file 197 | exec Run the selected commands 198 | help Help about any command 199 | list Show all snippets 200 | new Create a new snippet 201 | search Search snippets 202 | sync Sync snippets 203 | version Print the version number 204 | 205 | Flags: 206 | --config string config file (default is $HOME/.config/pet/config.toml) 207 | --debug debug mode 208 | 209 | Use "pet [command] --help" for more information about a command. 210 | ``` 211 | 212 | # Snippet 213 | Run `pet edit` 214 | You can also register the output of command (but cannot search). 215 | 216 | ``` 217 | [[snippets]] 218 | command = "echo | openssl s_client -connect example.com:443 2>/dev/null |openssl x509 -dates -noout" 219 | description = "Show expiration date of SSL certificate" 220 | output = """ 221 | notBefore=Nov 3 00:00:00 2015 GMT 222 | notAfter=Nov 28 12:00:00 2018 GMT""" 223 | ``` 224 | 225 | Run `pet list` 226 | 227 | ``` 228 | Command: echo | openssl s_client -connect example.com:443 2>/dev/null |openssl x509 -dates -noout 229 | Description: Show expiration date of SSL certificate 230 | Output: notBefore=Nov 3 00:00:00 2015 GMT 231 | notAfter=Nov 28 12:00:00 2018 GMT 232 | ------------------------------ 233 | ``` 234 | 235 | 236 | # Configuration 237 | 238 | Run `pet configure` 239 | 240 | ``` 241 | [General] 242 | snippetfile = "path/to/snippet" # specify snippet directory 243 | editor = "vim" # your favorite text editor 244 | column = 40 # column size for list command 245 | selectcmd = "fzf" # selector command for edit command (fzf or peco) 246 | backend = "gist" # specify backend service to sync snippets (gist or gitlab, default: gist) 247 | sortby = "description" # specify how snippets get sorted (recency (default), -recency, description, -description, command, -command, output, -output) 248 | cmd = ["sh", "-c"] # specify the command to execute the snippet with 249 | 250 | [Gist] 251 | file_name = "pet-snippet.toml" # specify gist file name 252 | access_token = "" # your access token 253 | gist_id = "" # Gist ID 254 | public = false # public or priate 255 | auto_sync = false # sync automatically when editing snippets 256 | 257 | [GitLab] 258 | file_name = "pet-snippet.toml" # specify GitLab Snippets file name 259 | access_token = "XXXXXXXXXXXXX" # your access token 260 | id = "" # GitLab Snippets ID 261 | visibility = "private" # public or internal or private 262 | auto_sync = false # sync automatically when editing snippets 263 | 264 | ``` 265 | 266 | ## Selector option 267 | Example1: Change layout (bottom up) 268 | 269 | ``` 270 | $ pet configure 271 | [General] 272 | ... 273 | selectcmd = "fzf" 274 | ... 275 | ``` 276 | 277 | Example2: Enable colorized output 278 | ``` 279 | $ pet configure 280 | [General] 281 | ... 282 | selectcmd = "fzf --ansi" 283 | ... 284 | $ pet search --color 285 | ``` 286 | 287 | ## Tag 288 | You can use tags (delimiter: space). 289 | ``` 290 | $ pet new -t 291 | Command> ping 8.8.8.8 292 | Description> ping 293 | Tag> network google 294 | ``` 295 | 296 | Or edit manually. 297 | ``` 298 | $ pet edit 299 | [[snippets]] 300 | description = "ping" 301 | command = "ping 8.8.8.8" 302 | tag = ["network", "google"] 303 | output = "" 304 | ``` 305 | 306 | They are displayed with snippets. 307 | ``` 308 | $ pet search 309 | [ping]: ping 8.8.8.8 #network #google 310 | ``` 311 | 312 | You can exec snipet with filtering the tag 313 | 314 | ``` 315 | $ pet exec -t google 316 | 317 | [ping]: ping 8.8.8.8 #network #google 318 | ``` 319 | 320 | ## Sync 321 | ### Gist 322 | You must obtain access token. 323 | Go https://github.com/settings/tokens/new and create access token (only need "gist" scope). 324 | Set that to `access_token` in `[Gist]` or use an environment variable with the name `$PET_GITHUB_ACCESS_TOKEN`. 325 | 326 | After setting, you can upload snippets to Gist. 327 | If `gist_id` is not set, new gist will be created. 328 | ``` 329 | $ pet sync 330 | Gist ID: 1cedddf4e06d1170bf0c5612fb31a758 331 | Upload success 332 | ``` 333 | 334 | Set `Gist ID` to `gist_id` in `[Gist]`. 335 | `pet sync` compares the local file and gist with the update date and automatically download or upload. 336 | 337 | If the local file is older than gist, `pet sync` download snippets. 338 | ``` 339 | $ pet sync 340 | Download success 341 | ``` 342 | 343 | If gist is older than the local file, `pet sync` upload snippets. 344 | ``` 345 | $ pet sync 346 | Upload success 347 | ``` 348 | 349 | *Note: `-u` option is deprecated* 350 | 351 | ### GitLab Snippets 352 | You must obtain access token. 353 | Go https://gitlab.com/-/profile/personal_access_tokens and create access token. 354 | Set that to `access_token` in `[GitLab]` or use an environment variable with the name `$PET_GITLAB_ACCESS_TOKEN`. 355 | 356 | You also have to configure the `url` under `[GitLab]`, so pet knows which endpoint to access. You would use `url = "https://gitlab.com"`unless you have another instance of Gitlab. 357 | 358 | At last, switch the `backend` under `[General]` to `backend = "gitlab"`. 359 | 360 | After setting, you can upload snippets to GitLab Snippets. 361 | If `id` is not set, new snippet will be created. 362 | ``` 363 | $ pet sync 364 | GitLab Snippet ID: 12345678 365 | Upload success 366 | ``` 367 | 368 | Set `GitLab Snippet ID` to `id` in `[GitLab]`. 369 | `pet sync` compares the local file and gitlab with the update date and automatically download or upload. 370 | 371 | If the local file is older than gitlab, `pet sync` download snippets. 372 | ``` 373 | $ pet sync 374 | Download success 375 | ``` 376 | 377 | If gitlab is older than the local file, `pet sync` upload snippets. 378 | ``` 379 | $ pet sync 380 | Upload success 381 | ``` 382 | 383 | ## Auto Sync 384 | You can sync snippets automatically. 385 | Set `true` to `auto_sync` in `[Gist]` or `[GitLab]`. 386 | Then, your snippets sync automatically when `pet new` or `pet edit`. 387 | 388 | ``` 389 | $ pet edit 390 | Getting Gist... 391 | Updating Gist... 392 | Upload success 393 | ``` 394 | 395 | # Installation 396 | You need to install selector command ([fzf](https://github.com/junegunn/fzf) or [peco](https://github.com/peco/peco)). 397 | `homebrew` install `fzf` automatically. 398 | 399 | ## Binary 400 | Go to [the releases page](https://github.com/knqyf263/pet/releases), find the version you want, and download the zip file. Unpack the zip file, and put the binary to somewhere you want (on UNIX-y systems, /usr/local/bin or the like). Make sure it has execution bits turned on. 401 | 402 | ## Mac OS X / Homebrew 403 | You can use homebrew on OS X. 404 | ``` 405 | $ brew install knqyf263/pet/pet 406 | ``` 407 | 408 | If you receive an error (`Error: knqyf263/pet/pet 64 already installed`) during `brew upgrade`, try the following command 409 | 410 | ``` 411 | $ brew unlink pet && brew uninstall pet 412 | ($ rm -rf /usr/local/Cellar/pet/64) 413 | $ brew install knqyf263/pet/pet 414 | ``` 415 | 416 | ## RedHat, CentOS 417 | Download rpm package from [the releases page](https://github.com/knqyf263/pet/releases) 418 | ``` 419 | $ sudo rpm -ivh https://github.com/knqyf263/pet/releases/download/v0.3.0/pet_0.3.0_linux_amd64.rpm 420 | ``` 421 | 422 | ## Debian, Ubuntu 423 | Download deb package from [the releases page](https://github.com/knqyf263/pet/releases) 424 | ``` 425 | $ wget https://github.com/knqyf263/pet/releases/download/v0.3.6/pet_0.3.6_linux_amd64.deb 426 | dpkg -i pet_0.3.6_linux_amd64.deb 427 | ``` 428 | 429 | ## Archlinux 430 | Two packages are available in [AUR](https://wiki.archlinux.org/index.php/Arch_User_Repository). 431 | You can install the package [from source](https://aur.archlinux.org/packages/pet-git): 432 | ``` 433 | $ yaourt -S pet-git 434 | ``` 435 | Or [from the binary](https://aur.archlinux.org/packages/pet-bin): 436 | ``` 437 | $ yaourt -S pet-bin 438 | ``` 439 | 440 | ## Build 441 | 442 | ``` 443 | $ mkdir -p $GOPATH/src/github.com/knqyf263 444 | $ cd $GOPATH/src/github.com/knqyf263 445 | $ git clone https://github.com/knqyf263/pet.git 446 | $ cd pet 447 | $ make install 448 | ``` 449 | 450 | # Migration 451 | ## From Keep 452 | https://blog.saltedbrain.org/2018/12/converting-keep-to-pet-snippets.html 453 | 454 | # Contribute 455 | 456 | 1. fork a repository: github.com/knqyf263/pet to github.com/you/repo 457 | 2. get original code: `go get github.com/knqyf263/pet` 458 | 3. work on original code 459 | 4. add remote to your repo: git remote add myfork https://github.com/you/repo.git 460 | 5. push your changes: git push myfork 461 | 6. create a new Pull Request 462 | 463 | - see [GitHub and Go: forking, pull requests, and go-getting](http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html) 464 | 465 | ---- 466 | 467 | # License 468 | MIT 469 | 470 | # Author 471 | Teppei Fukuda 472 | --------------------------------------------------------------------------------