From bfa34d5c06f1761cc9af853eb9d0f0634582f896 Mon Sep 17 00:00:00 2001 From: shylie Date: Sat, 20 Dec 2025 22:00:11 -0500 Subject: [PATCH] Add initial image support via kitty terminal protocol --- go.mod | 6 +++-- go.sum | 8 ++++++ pkg/serra/card.go | 61 ++++++++++++++++++++++++++++++++----------- pkg/serra/root.go | 1 + pkg/serra/scryfall.go | 16 +++++++++++- 5 files changed, 74 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 63fed6d..81b619b 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/charmbracelet/x/ansi v0.3.2 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/dolmen-go/kittyimg v0.0.0-20250610224728-874967bd8ea4 // indirect github.com/gabriel-vasile/mimetype v1.4.6 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect @@ -43,6 +44,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/muesli/termenv v0.15.2 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -57,8 +59,8 @@ require ( golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/term v0.25.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect golang.org/x/text v0.19.0 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 9e7f3eb..43d4240 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dolmen-go/kittyimg v0.0.0-20250610224728-874967bd8ea4 h1:KGRb+vxMx5pGsfDjDSW2Th+b2OEflb0yC3s0daCmiYU= +github.com/dolmen-go/kittyimg v0.0.0-20250610224728-874967bd8ea4/go.mod h1:2vk7ATPVcI7uW4Sh6PrSQvtO+Czmq8509xcg/y8Osd0= github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -81,6 +83,8 @@ github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8 github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -148,10 +152,14 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/pkg/serra/card.go b/pkg/serra/card.go index 2280d9f..d15d7ea 100644 --- a/pkg/serra/card.go +++ b/pkg/serra/card.go @@ -1,10 +1,16 @@ package serra import ( + "bytes" + "encoding/base64" "fmt" + "image/png" + "os" "sort" "strings" + "github.com/dolmen-go/kittyimg" + "github.com/nfnt/resize" "github.com/spf13/cobra" "go.mongodb.org/mongo-driver/bson" ) @@ -23,6 +29,7 @@ func init() { cardCmd.Flags().BoolVarP(&detail, "detail", "d", false, "Show details for cards (url)") cardCmd.Flags().BoolVarP(&reserved, "reserved", "w", false, "If card is on reserved list") cardCmd.Flags().BoolVarP(&foil, "foil", "f", false, "If card is foil list") + cardCmd.Flags().BoolVarP(&drawImg, "image", "g", false, "Draw card image using kitty format") rootCmd.AddCommand(cardCmd) } @@ -165,7 +172,11 @@ func Cards(rarity, set, sortby, name, oracle, cardType string, reserved, foil bo func showCardList(cards []Card, detail bool) { var total float64 - if detail { + if drawImg { + for _, card := range cards { + drawImage(&card) + } + } else if detail { for _, card := range cards { fmt.Printf("* %dx %s%s%s (%s/%s) %s%.2f%s %s %s %s\n", card.SerraCount+card.SerraCountFoil+card.SerraCountEtched, Purple, card.Name, Reset, card.Set, card.CollectorNumber, Yellow, card.getValue(false), getCurrency(), Background, strings.Replace(card.ScryfallURI, "?utm_source=api", "", 1), Reset) total = total + card.getValue(false)*float64(card.SerraCount) + card.getValue(true)*float64(card.SerraCountFoil) @@ -177,24 +188,44 @@ func showCardList(cards []Card, detail bool) { } } - fmt.Printf("\nTotal Value: %s%.2f%s%s\n", Yellow, total, getCurrency(), Reset) - + if !drawImg { + fmt.Printf("\nTotal Value: %s%.2f%s%s\n", Yellow, total, getCurrency(), Reset) + } } func showCardDetails(card *Card) error { - fmt.Printf("%s%s%s (%s/%s)\n", Purple, card.Name, Reset, card.Set, card.CollectorNumber) - fmt.Printf("Added: %s\n", stringToTime(card.SerraCreated)) - fmt.Printf("Rarity: %s\n", card.Rarity) - fmt.Printf("Scryfall: %s\n", strings.Replace(card.ScryfallURI, "?utm_source=api", "", 1)) + if drawImg { + drawImage(card) + } else { + fmt.Printf("%s%s%s (%s/%s)\n", Purple, card.Name, Reset, card.Set, card.CollectorNumber) + fmt.Printf("Added: %s\n", stringToTime(card.SerraCreated)) + fmt.Printf("Rarity: %s\n", card.Rarity) + fmt.Printf("Scryfall: %s\n", strings.Replace(card.ScryfallURI, "?utm_source=api", "", 1)) - fmt.Printf("\n%sCurrent Value%s\n", Green, Reset) - fmt.Printf("* Normal: %dx %s%.2f%s%s\n", card.SerraCount, Yellow, card.getValue(false), getCurrency(), Reset) - if card.SerraCountFoil > 0 { - fmt.Printf("* Foil: %dx %s%.2f%s%s\n", card.SerraCountFoil, Yellow, card.getValue(true), getCurrency(), Reset) + fmt.Printf("\n%sCurrent Value%s\n", Green, Reset) + fmt.Printf("* Normal: %dx %s%.2f%s%s\n", card.SerraCount, Yellow, card.getValue(false), getCurrency(), Reset) + if card.SerraCountFoil > 0 { + fmt.Printf("* Foil: %dx %s%.2f%s%s\n", card.SerraCountFoil, Yellow, card.getValue(true), getCurrency(), Reset) + } + + fmt.Printf("\n%sValue History%s\n", Green, Reset) + showPriceHistory(card.SerraPrices, "* ", false) + + fmt.Println() } - - fmt.Printf("\n%sValue History%s\n", Green, Reset) - showPriceHistory(card.SerraPrices, "* ", false) - fmt.Println() return nil } + +func drawImage(card *Card) { + fmt.Printf("%s - %s (%s/%s)\n", card.Name, card.SetName, card.Set, card.CollectorNumber) + data, err := base64.StdEncoding.DecodeString(card.SerraImage64) + if err == nil { + reader := bytes.NewReader(data) + img, err := png.Decode(reader) + img = resize.Resize(244, 340, img, resize.Bicubic) + if err == nil { + kittyimg.Fprintln(os.Stdout, img) + } + } + fmt.Println() +} diff --git a/pkg/serra/root.go b/pkg/serra/root.go index 9ed091e..94ebf48 100644 --- a/pkg/serra/root.go +++ b/pkg/serra/root.go @@ -17,6 +17,7 @@ var ( cmc int64 count int64 detail bool + drawImg bool foil bool format string interactive bool diff --git a/pkg/serra/scryfall.go b/pkg/serra/scryfall.go index c5a82e9..3983a32 100644 --- a/pkg/serra/scryfall.go +++ b/pkg/serra/scryfall.go @@ -2,6 +2,7 @@ package serra import ( "bytes" + "encoding/base64" "encoding/json" "fmt" "io" @@ -22,6 +23,7 @@ type Card struct { SerraPrices []PriceEntry `bson:"serra_prices"` SerraCreated primitive.DateTime `bson:"serra_created"` SerraUpdated primitive.DateTime `bson:"serra_updated"` + SerraImage64 string `bson:"serra_image"` Artist string `json:"artist"` ArtistIds []string `json:"artist_ids"` @@ -308,7 +310,6 @@ func fetchCard(setName, collectorNumber string) (*Card, error) { r := bytes.NewReader(body) decoder := json.NewDecoder(r) val := &Card{} - err = decoder.Decode(val) if err != nil { log.Fatalf("%s", err) @@ -321,6 +322,19 @@ func fetchCard(setName, collectorNumber string) (*Card, error) { val.Prices.Date = primitive.NewDateTimeFromTime(time.Now()) val.SerraPrices = append(val.SerraPrices, val.Prices) + imgResp, imgErr := http.Get(val.ImageUris.Png) + if imgErr != nil { + log.Fatalln(err) + return &Card{}, err + } + bytes, readErr := io.ReadAll(imgResp.Body) + if readErr != nil { + log.Fatalln(err) + return &Card{}, err + } + + val.SerraImage64 = base64.StdEncoding.EncodeToString(bytes) + return val, nil }