├── 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 | ![](https://political-capable-roll.glitch.me/get/@acgtooltracemoego?theme=rule34) 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 | ![gochiusa_rize](https://raw.githubusercontent.com/dreamjz/pics/main/pics/2023/202312042054552.jpg) 98 | 99 | ![1](https://raw.githubusercontent.com/dreamjz/pics/main/pics/2023/202312042051978.gif) 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 | ![](https://political-capable-roll.glitch.me/get/@acgtooltracemoego?theme=rule34) 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 | ![gochiusa_rize](https://raw.githubusercontent.com/dreamjz/pics/main/pics/2023/202312042054552.jpg) 100 | 101 | ![1](https://raw.githubusercontent.com/dreamjz/pics/main/pics/2023/202312042051978.gif) 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 | --------------------------------------------------------------------------------