├── test
└── img
│ ├── DB-Kai.png
│ ├── gochiusa_rize.jpg
│ ├── konosuba_aqua.jpg
│ └── Kokoa_Hoto_And_Chino_Kafuu_Reading_The_Go_Programming_Language.png
├── main.go
├── internal
├── tui
│ ├── init.go
│ ├── update.go
│ ├── view.go
│ ├── cmd.go
│ └── model.go
├── util
│ └── util.go
└── search
│ ├── resp.go
│ └── file.go
├── cmd
├── root.go
└── file.go
├── .gitignore
├── pkg
└── log
│ ├── log.go
│ └── log_test.go
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ ├── ci.yml
│ └── release.yml
├── LICENSE
├── .goreleaser.yaml
├── go.mod
├── .golangci.yml
├── README_ZH_CN.md
├── README.md
└── go.sum
/test/img/DB-Kai.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acgtools/trace-moe-go/HEAD/test/img/DB-Kai.png
--------------------------------------------------------------------------------
/test/img/gochiusa_rize.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acgtools/trace-moe-go/HEAD/test/img/gochiusa_rize.jpg
--------------------------------------------------------------------------------
/test/img/konosuba_aqua.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acgtools/trace-moe-go/HEAD/test/img/konosuba_aqua.jpg
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/acgtools/trace-moe-go/cmd"
4 |
5 | func main() {
6 | cmd.Execute()
7 | }
8 |
--------------------------------------------------------------------------------
/test/img/Kokoa_Hoto_And_Chino_Kafuu_Reading_The_Go_Programming_Language.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acgtools/trace-moe-go/HEAD/test/img/Kokoa_Hoto_And_Chino_Kafuu_Reading_The_Go_Programming_Language.png
--------------------------------------------------------------------------------
/internal/tui/init.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import tea "github.com/charmbracelet/bubbletea"
4 |
5 | func (m Model) Init() tea.Cmd {
6 | return tea.Batch(m.spinner.Tick, traceMoe(m.dataSrc, m.searchType))
7 | }
8 |
--------------------------------------------------------------------------------
/internal/util/util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import "io"
4 |
5 | func Close(w io.Closer, err *error) {
6 | // respect the existed err
7 | if e := w.Close(); *err == nil {
8 | *err = e
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | var version = "dev"
10 |
11 | // rootCmd represents the base command when called without any subcommands
12 | var rootCmd = &cobra.Command{
13 | Use: "moe-go",
14 | Short: "A TUI app for finding anime scene by image",
15 | Long: `A TUI app for finding anime scene by image, using trace.moe api `,
16 | Version: version,
17 | }
18 |
19 | func Execute() {
20 | rootCmd.CompletionOptions.DisableDefaultCmd = true
21 |
22 | err := rootCmd.Execute()
23 | if err != nil {
24 | os.Exit(1)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Dependency directories (remove the comment below to include it)
18 | # vendor/
19 |
20 | # Go workspace file
21 | go.work
22 |
23 | # goland files
24 | .idea
25 |
26 | dist/
27 |
--------------------------------------------------------------------------------
/pkg/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "fmt"
5 | "log/slog"
6 | "strings"
7 | )
8 |
9 | const (
10 | DebugLevel = slog.LevelDebug
11 | InfoLevel = slog.LevelInfo
12 | WarnLevel = slog.LevelWarn
13 | ErrorLevel = slog.LevelError
14 | )
15 |
16 | func ParseLevel(lvl string) (slog.Level, error) {
17 | switch strings.ToLower(lvl) {
18 | case "debug":
19 | return DebugLevel, nil
20 | case "info":
21 | return InfoLevel, nil
22 | case "warn":
23 | return WarnLevel, nil
24 | case "error":
25 | return ErrorLevel, nil
26 | default:
27 | return 0, fmt.Errorf("unrecognized level: %s", lvl)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Why is the feature needed?**
17 |
18 | **Example**
19 |
20 | **Additional context**
21 |
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **How To Reproduce**
14 | Steps to reproduce the bug
15 |
16 | **Expected behavior**
17 | A clear and concise description of what you expected to happen.
18 |
19 | **Screenshots**
20 | write `N/A` if there isn's
21 |
22 | **Environment (please complete the following information):**
23 | - OS:
24 | - Version:
25 |
26 | **Additional context**
27 |
28 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | workflow_call:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | permissions:
12 | contents: read
13 |
14 | jobs:
15 | ci:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout repository
19 | uses: actions/checkout@v4
20 |
21 | - name: Set up Go
22 | uses: actions/setup-go@v4
23 | with:
24 | go-version-file: go.mod
25 | cache: false
26 |
27 | - name: Run golangci-lint
28 | uses: golangci/golangci-lint-action@v3.7.0
29 | with:
30 | version: v1.54
31 | args: --verbose --timeout=3m --issues-exit-code=1
32 |
33 | - name: Test
34 | run: go test -v ./... -race -covermode=atomic
35 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | tags:
5 | - "*"
6 |
7 | permissions:
8 | contents: write
9 |
10 | jobs:
11 | ci:
12 | uses: ./.github/workflows/ci.yml
13 |
14 | goreleaser:
15 | needs: ci
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v3
20 | with:
21 | fetch-depth: 0
22 | - name: Set up Go
23 | uses: actions/setup-go@v4
24 | with:
25 | go-version-file: go.mod
26 |
27 | - name: Release
28 | env:
29 | Version: $GITHUB_REF_NAME
30 | GITHUB_TOKEN: ${{ secrets.TRACE_MOE_GO_TOKEN }}
31 |
32 | uses: goreleaser/goreleaser-action@v5
33 | with:
34 | distribution: goreleaser
35 | version: latest
36 | args: release --clean
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 dreamjz
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/file.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/acgtools/trace-moe-go/internal/tui"
9 | tea "github.com/charmbracelet/bubbletea"
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | const minArgNum = 1
14 |
15 | var fileCmd = &cobra.Command{
16 | Use: "file",
17 | Short: "search image by file",
18 | Long: `file [flags] [image file path]`,
19 | RunE: func(cmd *cobra.Command, args []string) error {
20 | if len(args) < minArgNum {
21 | return errors.New("not enough arguments")
22 | }
23 |
24 | filePath := args[0]
25 | info, err := os.Lstat(filePath)
26 | if err != nil {
27 | return fmt.Errorf("invalid path: %w", err)
28 | }
29 |
30 | if info.IsDir() {
31 | return errors.New("image cannot be a directory")
32 | }
33 |
34 | m := tui.New(filePath, tui.File)
35 | var opts []tea.ProgramOption
36 |
37 | // Always append alt screen program option.
38 | opts = append(opts, tea.WithAltScreen())
39 |
40 | if _, err := tea.NewProgram(m, opts...).Run(); err != nil {
41 | return err
42 | }
43 |
44 | return nil
45 | },
46 | }
47 |
48 | func init() {
49 | rootCmd.AddCommand(fileCmd)
50 | }
51 |
--------------------------------------------------------------------------------
/internal/search/resp.go:
--------------------------------------------------------------------------------
1 | package search
2 |
3 | import "encoding/json"
4 |
5 | type TraceMoeResponse struct {
6 | FrameCount int `json:"frameCount"`
7 | Error string `json:"error"`
8 | Result []*Result `json:"result"`
9 | }
10 |
11 | type Result struct {
12 | Anilist *Anilist `json:"anilist"`
13 | Filename string `json:"filename"`
14 | Episode Episode `json:"episode"`
15 | From float64 `json:"from"`
16 | To float64 `json:"to"`
17 | Similarity float64 `json:"similarity"`
18 | Video string `json:"video"`
19 | Image string `json:"image"`
20 | }
21 |
22 | type Episode string
23 |
24 | var _ json.Unmarshaler = (*Episode)(nil)
25 |
26 | func (ep *Episode) UnmarshalJSON(data []byte) error {
27 | *ep = Episode(data)
28 | return nil
29 | }
30 |
31 | type Anilist struct {
32 | ID int `json:"id"`
33 | IDMal int `json:"idMal"`
34 | Title *Title `json:"title"`
35 | Synonyms []string `json:"synonyms"`
36 | IsAdult bool `json:"isAdult"`
37 | }
38 |
39 | type Title struct {
40 | Native string `json:"native"`
41 | Romaji string `json:"romaji"`
42 | English string `json:"english"`
43 | }
44 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | before:
2 | hooks:
3 | - go mod tidy
4 |
5 | builds:
6 | - id: trace-moe-go
7 | binary: moe-go
8 | env:
9 | - CGO_ENABLED=0
10 | ldflags:
11 | - -s -w
12 | - -X github.com/acgtools/trace-moe-go/cmd.version={{ .Version }}
13 | goos:
14 | - linux
15 | - windows
16 | - darwin
17 | goarch:
18 | - "386"
19 | - amd64
20 | - arm64
21 | ignore:
22 | - goos: darwin
23 | goarch: "386"
24 | - goos: windows
25 | goarch: arm64
26 |
27 | archives:
28 | - format: tar.gz
29 | # this name template makes the OS and Arch compatible with the results of `uname`.
30 | name_template: >-
31 | {{ .ProjectName }}_
32 | {{- title .Os }}_
33 | {{- if eq .Arch "amd64" }}x86_64
34 | {{- else if eq .Arch "386" }}i386
35 | {{- else }}{{ .Arch }}{{ end }}
36 | {{- if .Arm }}v{{ .Arm }}{{ end }}
37 | # use zip for windows archives
38 | format_overrides:
39 | - goos: windows
40 | format: zip
41 |
42 | changelog:
43 | sort: asc
44 | filters:
45 | exclude:
46 | - "^docs:"
47 | - "^test:"
48 |
49 | release:
50 | draft: true
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/acgtools/trace-moe-go
2 |
3 | go 1.21
4 |
5 | require (
6 | github.com/charmbracelet/bubbles v0.16.1
7 | github.com/charmbracelet/bubbletea v0.24.2
8 | github.com/charmbracelet/lipgloss v0.7.1
9 | github.com/disintegration/imaging v1.6.2
10 | github.com/lucasb-eyer/go-colorful v1.2.0
11 | github.com/spf13/cobra v1.8.0
12 | github.com/stretchr/testify v1.8.4
13 | )
14 |
15 | require (
16 | github.com/atotto/clipboard v0.1.4 // indirect
17 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
18 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
19 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
20 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
21 | github.com/kr/pretty v0.3.1 // indirect
22 | github.com/mattn/go-isatty v0.0.18 // indirect
23 | github.com/mattn/go-localereader v0.0.1 // indirect
24 | github.com/mattn/go-runewidth v0.0.14 // indirect
25 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
26 | github.com/muesli/cancelreader v0.2.2 // indirect
27 | github.com/muesli/reflow v0.3.0 // indirect
28 | github.com/muesli/termenv v0.15.1 // indirect
29 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
30 | github.com/rivo/uniseg v0.2.0 // indirect
31 | github.com/sahilm/fuzzy v0.1.0 // indirect
32 | github.com/spf13/pflag v1.0.5 // indirect
33 | golang.org/x/image v0.1.0 // indirect
34 | golang.org/x/sync v0.3.0 // indirect
35 | golang.org/x/sys v0.12.0 // indirect
36 | golang.org/x/term v0.6.0 // indirect
37 | golang.org/x/text v0.13.0 // indirect
38 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
39 | gopkg.in/yaml.v3 v3.0.1 // indirect
40 | )
41 |
--------------------------------------------------------------------------------
/pkg/log/log_test.go:
--------------------------------------------------------------------------------
1 | package log_test
2 |
3 | import (
4 | "log/slog"
5 | "testing"
6 |
7 | "github.com/acgtools/trace-moe-go/pkg/log"
8 | "github.com/stretchr/testify/assert"
9 | "github.com/stretchr/testify/require"
10 | )
11 |
12 | func TestParseLevel(t *testing.T) {
13 | t.Parallel()
14 |
15 | for _, tc := range []struct {
16 | name string
17 | lvl string
18 | parsedLevel slog.Level
19 | wantErr bool
20 | }{
21 | {
22 | name: "Empty string",
23 | lvl: "",
24 | parsedLevel: 0,
25 | wantErr: true,
26 | },
27 | {
28 | name: "Uppercase level",
29 | lvl: "DEBUG",
30 | parsedLevel: log.DebugLevel,
31 | wantErr: false,
32 | },
33 | {
34 | name: "Debug",
35 | lvl: "debug",
36 | parsedLevel: log.DebugLevel,
37 | wantErr: false,
38 | },
39 | {
40 | name: "Info",
41 | lvl: "info",
42 | parsedLevel: log.InfoLevel,
43 | wantErr: false,
44 | },
45 | {
46 | name: "Warn",
47 | lvl: "warn",
48 | parsedLevel: log.WarnLevel,
49 | wantErr: false,
50 | },
51 | {
52 | name: "Error",
53 | lvl: "error",
54 | parsedLevel: log.ErrorLevel,
55 | wantErr: false,
56 | },
57 | {
58 | name: "Unsupported level",
59 | lvl: "XXX",
60 | parsedLevel: 0,
61 | wantErr: true,
62 | },
63 | } {
64 | tc := tc
65 | t.Run(tc.name, func(t *testing.T) {
66 | t.Parallel()
67 |
68 | l, err := log.ParseLevel(tc.lvl)
69 |
70 | assert.Equal(t, tc.parsedLevel, l)
71 |
72 | if tc.wantErr {
73 | require.Error(t, err)
74 | require.ErrorContains(t, err, "unrecognized level: ")
75 | } else {
76 | require.NoError(t, err)
77 | }
78 | })
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | ---
2 | linters:
3 | enable-all: true
4 | disable:
5 | - maligned # WARN [runner] The linter 'maligned' is deprecated (since v1.38.0) due to: The repository of the linter has been archived by the owner. Replaced by govet 'fieldalignment'.
6 | - interfacer # WARN [runner] The linter 'interfacer' is deprecated (since v1.38.0) due to: The repository of the linter has been archived by the owner.
7 | - scopelint # WARN [runner] The linter 'scopelint' is deprecated (since v1.39.0) due to: The repository of the linter has been deprecated by the owner. Replaced by exportloopref.
8 | - nosnakecase # WARN [runner] The linter 'nosnakecase' is deprecated (since v1.48.1) due to: The repository of the linter has been deprecated by the owner. Replaced by revive(var-naming).
9 | - deadcode # WARN [runner] The linter 'deadcode' is deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
10 | - structcheck # WARN [runner] The linter 'structcheck' is deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
11 | - ifshort # WARN [runner] The linter 'ifshort' is deprecated (since v1.48.0) due to: The repository of the linter has been deprecated by the owner.
12 | - varcheck # WARN [runner] The linter 'varcheck' is deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
13 | - wsl
14 | - goerr113
15 | - lll
16 | - godot
17 | - nlreturn
18 | - godox
19 | - golint
20 | - tagliatelle
21 | - exhaustivestruct # https://github.com/mbilski/exhaustivestruct
22 | - ireturn
23 | - varnamelen
24 | - exhaustive
25 | - exhaustruct
26 | - nonamedreturns
27 | - musttag
28 | - depguard
29 | - gochecknoglobals
30 | - wrapcheck
31 | - gochecknoinits
32 | - gomnd
33 | - errname
34 |
35 | issues:
36 | exclude-rules:
37 | - path: _test\.go
38 | linters:
39 | - funlen
40 | - govet
41 | - cyclop
42 |
--------------------------------------------------------------------------------
/internal/tui/update.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "github.com/acgtools/trace-moe-go/internal/search"
5 | tea "github.com/charmbracelet/bubbletea"
6 | )
7 |
8 | type successMsg struct {
9 | resp *search.TraceMoeResponse
10 | }
11 |
12 | type errMsg struct {
13 | err error
14 | }
15 |
16 | var _ error = errMsg{}
17 |
18 | func (e errMsg) Error() string {
19 | return e.err.Error()
20 | }
21 |
22 | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:cyclop
23 | var (
24 | cmd tea.Cmd
25 | cmds []tea.Cmd
26 | )
27 |
28 | switch msg := msg.(type) {
29 | case tea.WindowSizeMsg:
30 | m.size = size{
31 | width: msg.Width,
32 | height: msg.Height,
33 | }
34 | m.imgWidth = msg.Width / 3
35 | img, err := imgToString(m.imgWidth, m.dataSrc, m.searchType)
36 | if err != nil {
37 | return m, errCmd(err)
38 | }
39 | m.image = img
40 | case tea.KeyMsg:
41 | switch msg.String() {
42 | case "q":
43 | return m, tea.Quit
44 | case " ", "enter":
45 | if m.ready {
46 | res := m.resp.Result[m.resList.Index()]
47 | m.setInfo(res)
48 | err := m.setPreview(res)
49 | if err != nil {
50 | return m, errCmd(err)
51 | }
52 | }
53 | }
54 | case successMsg:
55 | m.ready = true
56 | m.resp = msg.resp
57 | m.resList = newList(msg.resp.Result, m.size)
58 | if len(msg.resp.Result) > 0 {
59 | res := m.resp.Result[0]
60 | m.setInfo(res)
61 | err := m.setPreview(res)
62 | if err != nil {
63 | return m, errCmd(err)
64 | }
65 | }
66 | case errMsg:
67 | m.err = msg.err
68 | }
69 |
70 | m.spinner, cmd = m.spinner.Update(msg)
71 | cmds = append(cmds, cmd)
72 |
73 | if m.ready {
74 | m.resList, cmd = m.resList.Update(msg)
75 | cmds = append(cmds, cmd)
76 | }
77 |
78 | return m, tea.Batch(cmds...)
79 | }
80 |
81 | func (m *Model) setInfo(res *search.Result) {
82 | m.info = newAnimeInfo(res.Anilist)
83 | }
84 |
85 | func (m *Model) setPreview(res *search.Result) error {
86 | img, err := imgToString(m.imgWidth, res.Image, Link)
87 | if err != nil {
88 | return err
89 | }
90 | m.matchPreview = img
91 |
92 | return nil
93 | }
94 |
--------------------------------------------------------------------------------
/internal/search/file.go:
--------------------------------------------------------------------------------
1 | package search
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "errors"
8 | "fmt"
9 | "io"
10 | "mime/multipart"
11 | "net/http"
12 | "os"
13 | "path/filepath"
14 |
15 | "github.com/acgtools/trace-moe-go/internal/util"
16 | )
17 |
18 | const (
19 | fileSearchURL = "https://api.trace.moe/search?anilistInfo"
20 | )
21 |
22 | func File(filePath string) (*TraceMoeResponse, error) {
23 | var err error
24 |
25 | res, err := post(filePath)
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | var traceResp TraceMoeResponse
31 | err = json.Unmarshal(res, &traceResp)
32 | if err != nil {
33 | return nil, fmt.Errorf("unmarshal response: %w", err)
34 | }
35 | return &traceResp, err
36 | }
37 |
38 | func post(filePath string) ([]byte, error) {
39 | var (
40 | err error
41 | res []byte
42 | )
43 |
44 | img, err := os.Open(filePath)
45 | if err != nil {
46 | return res, fmt.Errorf("open file %q: %w", filePath, err)
47 | }
48 |
49 | body := &bytes.Buffer{}
50 | mw := multipart.NewWriter(body)
51 | part, err := mw.CreateFormFile("image", filepath.Base(filePath))
52 | if err != nil {
53 | return res, fmt.Errorf("create form data: %w", err)
54 | }
55 |
56 | _, err = io.Copy(part, img)
57 | if err != nil {
58 | return res, fmt.Errorf("upload file: %w", err)
59 | }
60 |
61 | err = mw.Close()
62 | if err != nil {
63 | return res, fmt.Errorf("close multipart wirter: %w", err)
64 | }
65 |
66 | req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, fileSearchURL, body)
67 | if err != nil {
68 | return res, fmt.Errorf("create request: %w", err)
69 | }
70 | req.Header.Set("Content-Type", mw.FormDataContentType())
71 |
72 | resp, err := http.DefaultClient.Do(req) //nolint:bodyclose
73 | if err != nil {
74 | return res, fmt.Errorf("send post request to %q: %w", fileSearchURL, err)
75 | }
76 | defer util.Close(resp.Body, &err)
77 |
78 | if err != nil {
79 | return res, fmt.Errorf("send post request to %q: %w", fileSearchURL, err)
80 | }
81 | defer util.Close(resp.Body, &err)
82 |
83 | res, err = io.ReadAll(resp.Body)
84 | if err != nil {
85 | return res, fmt.Errorf("read respsonse: %w", err)
86 | }
87 |
88 | if resp.StatusCode != http.StatusOK {
89 | return res, errors.New(resp.Status)
90 | }
91 |
92 | return res, err
93 | }
94 |
--------------------------------------------------------------------------------
/internal/tui/view.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/charmbracelet/lipgloss"
8 | )
9 |
10 | var (
11 | layoutStyle = lipgloss.NewStyle()
12 | imageStyle = lipgloss.NewStyle()
13 | previewImgStyle = lipgloss.NewStyle()
14 | listStyle = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder())
15 | videoStyle = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder())
16 | infoStyle = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder())
17 | errStyle = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder())
18 | )
19 |
20 | func (m Model) View() string {
21 | if m.err != nil {
22 | return errStyle.Render(fmt.Sprintf("%v\npress q to exit", m.err))
23 | }
24 |
25 | var sb strings.Builder
26 |
27 | if !m.ready {
28 | sb.WriteString(fmt.Sprintf("%s %s\n", "Searching...", m.spinner.View()))
29 | sb.WriteString("help - q: exit")
30 | } else {
31 | infoWidth, infoHeight := m.size.width/4, m.resList.Height()
32 |
33 | image := imageStyle.Render("Your search image\n" + m.image)
34 | previewImg := previewImgStyle.Render("Matched image preview\n" + m.matchPreview)
35 |
36 | list := listStyle.Render(m.resList.View())
37 |
38 | videoStyle = videoStyle.Width(infoWidth).Height(10).Align(lipgloss.Center, lipgloss.Center)
39 | video := videoStyle.Render("Assuming there is a video here. >_<")
40 |
41 | infoStyle = infoStyle.Width(infoWidth).Height(infoHeight - 12)
42 | info := infoStyle.Render(formatAnimeInfo(m.info))
43 |
44 | output := layout(image, previewImg, list, video, info)
45 | layoutStyle = layoutStyle.MaxWidth(m.size.width).MaxHeight(m.size.height)
46 |
47 | sb.WriteString(layoutStyle.Render(output))
48 | }
49 |
50 | return sb.String()
51 | }
52 |
53 | func layout(image, previewImg, list, video, info string) string {
54 | right := lipgloss.JoinVertical(lipgloss.Top, video, info)
55 | images := lipgloss.JoinVertical(lipgloss.Top, image, previewImg)
56 | return lipgloss.JoinHorizontal(lipgloss.Top, list, right, images)
57 | }
58 |
59 | func formatAnimeInfo(info animeInfo) string {
60 | var sb strings.Builder
61 |
62 | sb.WriteString(fmt.Sprintf("Anilist ID: %d\n\n", info.anilistID))
63 | sb.WriteString(fmt.Sprintf("Is adult: %t\n\n", info.isAdult))
64 |
65 | for _, name := range info.names {
66 | sb.WriteString(name + "\n\n")
67 | }
68 |
69 | return sb.String()
70 | }
71 |
--------------------------------------------------------------------------------
/README_ZH_CN.md:
--------------------------------------------------------------------------------
1 | # trace-moe-go
2 |
3 | 用于搜索番剧图片来源的 TUI 应用,使用 [trace.moe](https://trace.moe/) API。
4 |
5 | 如果这个程序对你有所帮助,可以帮忙给一个 star (o゜▽゜)o☆ ,谢谢 OwO。
6 |
7 | > 随机 Wink OvO
8 |
9 |
10 |
11 |
12 |
13 |
14 |
20 | 
21 |
22 | ## 安装
23 |
24 | ### 使用 `go`
25 |
26 | ```sh
27 | $ go install -ldflags "-s -w" github.com/acgtools/trace-moe-go
28 | ```
29 |
30 | ### 从 Release 页面下载
31 |
32 | [release page](https://github.com/acgtools/trace-moe-go/releases)
33 |
34 | ## 快速开始
35 |
36 | ```sh
37 | $ moe-go -h
38 | A TUI app for finding anime scene by image, using trace.moe api
39 |
40 | Usage:
41 | moe-go [command]
42 |
43 | Available Commands:
44 | file search image by file
45 | help Help about any command
46 |
47 | Flags:
48 | -h, --help help for moe-go
49 | -v, --version version for moe-go
50 |
51 | Use "moe-go [command] --help" for more information about a command.
52 | ```
53 |
54 | ### 确保你的终端字符集为 UTF-8
55 |
56 | #### Windows
57 |
58 | ```cmd
59 | > chcp
60 | Active code page: 65001
61 |
62 | # 如果输出结果不是 65001(utf-8), 可以临时改变当前字符集
63 | > chcp 65001
64 | ```
65 |
66 | 如果你想修改默认的字符集, 按照以下步骤:
67 |
68 | 1. 开始 -> 运行 -> regedit
69 | 2. 找到 `[HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\Autorun]`
70 | 3. 将其值修改为 `@chcp 65001>nul`
71 |
72 | 如果 `Autorun` 不存在, 你可以创建一个新的字符串类型的键值对.
73 |
74 | 此方法将在`cmd` 启动时自动执行 `@chcp 65001>nul`。
75 |
76 | #### Linux
77 |
78 | ```sh
79 | $ echo $LANG
80 | en_US.UTF-8
81 | ```
82 |
83 | ### 搜索图片文件
84 |
85 | ```sh
86 | $ moe-go file
87 | ```
88 |
89 | 按键:
90 |
91 | - `up`, `down`: 移动
92 | - `space` ,`enter`: 选择一个查找结果
93 | - `q`: 退出程序
94 |
95 | #### 示例
96 |
97 | 
98 |
99 | 
100 |
101 | ## Issue
102 |
103 | 欢迎创建 issue 来报告 bug 或者 请求添加新特性。
104 |
--------------------------------------------------------------------------------
/internal/tui/cmd.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "image"
5 | "io"
6 | "net/http"
7 | "os"
8 | "path/filepath"
9 | "strings"
10 |
11 | "github.com/acgtools/trace-moe-go/internal/search"
12 | "github.com/acgtools/trace-moe-go/internal/util"
13 | tea "github.com/charmbracelet/bubbletea"
14 | "github.com/charmbracelet/lipgloss"
15 | "github.com/disintegration/imaging"
16 | "github.com/lucasb-eyer/go-colorful"
17 | )
18 |
19 | func traceMoe(dataSrc string, typ searchType) tea.Cmd {
20 | return func() tea.Msg {
21 | var (
22 | res *search.TraceMoeResponse
23 | err error
24 | )
25 |
26 | if typ == File {
27 | res, err = search.File(dataSrc)
28 | }
29 |
30 | if err != nil {
31 | return errMsg{err}
32 | }
33 |
34 | return successMsg{resp: res}
35 | }
36 | }
37 |
38 | func imgToString(width int, dataSrc string, typ searchType) (string, error) {
39 | var (
40 | imageContent io.ReadCloser
41 | err error
42 | )
43 |
44 | if typ == File {
45 | imageContent, err = os.Open(filepath.Clean(dataSrc))
46 | } else {
47 | imageContent, err = fetchCoverImage(dataSrc)
48 | }
49 |
50 | if err != nil {
51 | return "", err
52 | }
53 | defer util.Close(imageContent, &err)
54 |
55 | img, _, err := image.Decode(imageContent)
56 | if err != nil {
57 | return "", err
58 | }
59 |
60 | return toString(width, img), err
61 | }
62 |
63 | func toString(width int, img image.Image) string {
64 | img = imaging.Resize(img, width, 0, imaging.Lanczos)
65 | b := img.Bounds()
66 | imageWidth := b.Max.X
67 | h := b.Max.Y
68 | str := strings.Builder{}
69 |
70 | for heightCounter := 0; heightCounter < h; heightCounter += 2 {
71 | for x := imageWidth; x < width; x += 2 {
72 | str.WriteString(" ")
73 | }
74 |
75 | for x := 0; x < imageWidth; x++ {
76 | c1, _ := colorful.MakeColor(img.At(x, heightCounter))
77 | color1 := lipgloss.Color(c1.Hex())
78 | c2, _ := colorful.MakeColor(img.At(x, heightCounter+1))
79 | color2 := lipgloss.Color(c2.Hex())
80 | str.WriteString(lipgloss.NewStyle().Foreground(color1).
81 | Background(color2).Render("▀"))
82 | }
83 |
84 | str.WriteString("\n")
85 | }
86 |
87 | return str.String()
88 | }
89 |
90 | func fetchCoverImage(imgURL string) (io.ReadCloser, error) {
91 | resp, err := http.Get(imgURL) //nolint:gosec,noctx
92 | if err != nil {
93 | return nil, err
94 | }
95 |
96 | return resp.Body, err
97 | }
98 |
99 | func errCmd(err error) tea.Cmd {
100 | return func() tea.Msg {
101 | return errMsg{err}
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # trace-moe-go
2 |
3 | English | [简体中文](./README_ZH_CN.md)
4 |
5 | A TUI app for finding anime scenes by image using [trace.moe](https://trace.moe/) API.
6 |
7 | If this repo is helpful to you, please consider giving it a star (o゜▽゜)o☆ . Thank you OwO.
8 |
9 | > Random Wink OvO
10 |
11 |
12 |
13 |
14 |
15 |
16 |
22 | 
23 |
24 | ## Installation
25 |
26 | ### Using `go`
27 |
28 | ```sh
29 | $ go install -ldflags "-s -w" github.com/acgtools/trace-moe-go
30 | ```
31 |
32 | ### Download from releases
33 |
34 | [release page](https://github.com/acgtools/trace-moe-go/releases)
35 |
36 | ## Quick Start
37 |
38 | ```sh
39 | $ moe-go -h
40 | A TUI app for finding anime scene by image, using trace.moe api
41 |
42 | Usage:
43 | moe-go [command]
44 |
45 | Available Commands:
46 | file search image by file
47 | help Help about any command
48 |
49 | Flags:
50 | -h, --help help for moe-go
51 | -v, --version version for moe-go
52 |
53 | Use "moe-go [command] --help" for more information about a command.
54 | ```
55 |
56 | ### Ensure your terminal charset is UTF-8
57 |
58 | #### Windows
59 |
60 | ```cmd
61 | > chcp
62 | Active code page: 65001
63 |
64 | # if code page is not 65001(utf-8), change it temporarily
65 | > chcp 65001
66 | ```
67 |
68 | If you want to set the default charset, follow the steps:
69 |
70 | 1. Start -> Run -> regedit
71 | 2. Go to `[HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\Autorun]`
72 | 3. Change the value to `@chcp 65001>nul`
73 |
74 | If `Autorun` is not present, you can add a `New String`.
75 |
76 | This approach will auto-execute `@chcp 65001>nul` when `cmd` starts.
77 |
78 | #### Linux
79 |
80 | ```sh
81 | $ echo $LANG
82 | en_US.UTF-8
83 | ```
84 |
85 | ### Find by image file
86 |
87 | ```sh
88 | $ moe-go file
89 | ```
90 |
91 | Keys:
92 |
93 | - `up`, `down`: move the cursor
94 | - `space` ,`enter`: select one result
95 | - `q`: quit program
96 |
97 | #### Example
98 |
99 | 
100 |
101 | 
102 |
103 | ## Issue
104 |
105 | Feel free to create issues to report bugs or request new features.
106 |
--------------------------------------------------------------------------------
/internal/tui/model.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | "strings"
7 |
8 | "github.com/acgtools/trace-moe-go/internal/search"
9 | "github.com/charmbracelet/bubbles/list"
10 | "github.com/charmbracelet/bubbles/spinner"
11 | tea "github.com/charmbracelet/bubbletea"
12 | )
13 |
14 | type searchType uint8
15 |
16 | const (
17 | File searchType = iota
18 | Link
19 | )
20 |
21 | type Model struct {
22 | spinner spinner.Model
23 | dataSrc string
24 | searchType searchType
25 | resp *search.TraceMoeResponse
26 | image string
27 | matchPreview string
28 | ready bool
29 | err error
30 | resList list.Model
31 | info animeInfo
32 | size size
33 | imgWidth int
34 | }
35 |
36 | type size struct {
37 | width int
38 | height int
39 | }
40 |
41 | type animeInfo struct {
42 | anilistID int
43 | names []string
44 | isAdult bool
45 | }
46 |
47 | func newAnimeInfo(ani *search.Anilist) animeInfo {
48 | var names []string
49 | names = append(names,
50 | ani.Title.Native,
51 | ani.Title.Romaji,
52 | ani.Title.English)
53 | names = append(names, ani.Synonyms...)
54 |
55 | return animeInfo{
56 | anilistID: ani.ID,
57 | names: names,
58 | isAdult: ani.IsAdult,
59 | }
60 | }
61 |
62 | var _ tea.Model = Model{}
63 |
64 | func New(dataSrc string, typ searchType) Model {
65 | return Model{
66 | spinner: spinner.New(),
67 | dataSrc: dataSrc,
68 | searchType: typ,
69 | }
70 | }
71 |
72 | type resultItem struct {
73 | index int
74 | episode search.Episode
75 | name string
76 | from float64
77 | to float64
78 | similarity float64
79 | }
80 |
81 | func (i resultItem) Title() string { return i.name }
82 |
83 | func (i resultItem) Description() string {
84 | var sb strings.Builder
85 |
86 | sb.WriteString(fmt.Sprintf("Episode: %s\n", i.episode))
87 | sb.WriteString(fmt.Sprintf("From %s to %s\n", formatTime(i.from), formatTime(i.to)))
88 | sb.WriteString(fmt.Sprintf("Similarity: %.3f%s", i.similarity*100, "%"))
89 |
90 | return sb.String()
91 | }
92 |
93 | func (i resultItem) FilterValue() string { return i.name }
94 |
95 | func newList(results []*search.Result, size size) list.Model {
96 | items := make([]list.Item, 0, len(results))
97 | for i, res := range results {
98 | item := resultItem{
99 | index: i,
100 | episode: res.Episode,
101 | name: res.Anilist.Title.Native,
102 | from: res.From,
103 | to: res.To,
104 | similarity: res.Similarity,
105 | }
106 | items = append(items, item)
107 | }
108 |
109 | delegate := list.NewDefaultDelegate()
110 | delegate.SetHeight(4)
111 |
112 | w, h := listStyle.GetFrameSize()
113 | l := list.New(items, delegate, size.width-w, size.height-h)
114 | l.Title = "Result list"
115 |
116 | return l
117 | }
118 |
119 | func formatTime(secs float64) string {
120 | seconds := int(math.Round(secs))
121 | m, s := seconds/60, seconds%60
122 | return fmt.Sprintf("%d:%d", m, s)
123 | }
124 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
4 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
5 | github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY=
6 | github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=
7 | github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY=
8 | github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
9 | github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
10 | github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
11 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
12 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
13 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
14 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
15 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
16 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
17 | github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
18 | github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
19 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
20 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
21 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
22 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
23 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
24 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
25 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
26 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
27 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
28 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
29 | github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
30 | github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
31 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
32 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
33 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
34 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
35 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
36 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
37 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
38 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
39 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
40 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
41 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
42 | github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
43 | github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
44 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
45 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
46 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
47 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
48 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
49 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
50 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
51 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
52 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
53 | github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
54 | github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
55 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
56 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
57 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
58 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
59 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
60 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
61 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
62 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
63 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
64 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
65 | golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
66 | golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
67 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
68 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
69 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
70 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
71 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
72 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
73 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
74 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
75 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
76 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
77 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
78 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
79 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
80 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
81 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
82 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
83 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
84 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
85 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
86 | golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
87 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
88 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
89 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
90 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
91 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
92 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
93 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
94 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
95 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
96 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
97 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
98 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
99 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
100 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
101 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
102 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
103 |
--------------------------------------------------------------------------------