From 433e3c20d596dd8215da8d33ef0df6e06e7f6fca Mon Sep 17 00:00:00 2001 From: Spenser Black Date: Wed, 22 Nov 2023 10:12:55 -0500 Subject: [PATCH 1/3] Add cobra CLI --- cmd/cmd.go | 18 ++++++++++++++++++ cmd/root.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 7 ++++++- go.sum | 8 ++++++++ main.go | 51 ++------------------------------------------------- 5 files changed, 85 insertions(+), 50 deletions(-) create mode 100644 cmd/cmd.go create mode 100644 cmd/root.go diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..c18794b --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,18 @@ +// Package cmd contains the command line interface for the application. +package cmd + +import ( + "fmt" + "os" +) + +func Execute() { + onError(rootCmd.Execute()) +} + +func onError(err error) { + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..cc58f14 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,51 @@ +package cmd + +import ( + "image" + "image/png" + "os" + + "github.com/cli/go-gh/v2/pkg/api" + "github.com/cli/go-gh/v2/pkg/repository" + "github.com/spenserblack/gh-collab-montage/pkg/avatar" + "github.com/spenserblack/gh-collab-montage/pkg/avatar/grid" + "github.com/spenserblack/gh-collab-montage/pkg/usersource" + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "gh-collab-montage", + Short: "Combine your contributors avatars into a single image", + Run: func(cmd *cobra.Command, args []string) { + f, err := os.Create("montage.png") + defer f.Close() + onError(err) + client, err := api.DefaultRESTClient() + onError(err) + repository, err := repository.Current() + onError(err) + source := usersource.NewContributors(client, repository.Owner, repository.Name) + avatars := []image.Image{} + for { + user, stop, err := source.Next() + onError(err) + if stop { + break + } + if user.Type != "User" { + continue + } + a, err := avatar.Decode(user.AvatarURL) + onError(err) + avatars = append(avatars, a) + } + g := grid.NewWithSize(len(avatars)) + for _, a := range avatars { + g.AddAvatar(a) + } + + m := g.Image() + err = png.Encode(f, m) + onError(err) + }, +} diff --git a/go.mod b/go.mod index 4d2da1f..79fd03d 100644 --- a/go.mod +++ b/go.mod @@ -2,19 +2,24 @@ module github.com/spenserblack/gh-collab-montage go 1.18 -require github.com/cli/go-gh/v2 v2.4.0 +require ( + github.com/cli/go-gh/v2 v2.4.0 + github.com/spf13/cobra v1.8.0 +) require ( github.com/aymanbagabas/go-osc52 v1.0.3 // indirect github.com/cli/safeexec v1.0.0 // indirect github.com/cli/shurcooL-graphql v0.0.4 // indirect github.com/henvic/httpretty v0.0.6 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/muesli/termenv v0.13.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect diff --git a/go.sum b/go.sum index d0e3fd8..aafa202 100644 --- a/go.sum +++ b/go.sum @@ -7,11 +7,14 @@ github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJu2GuY= github.com/cli/shurcooL-graphql v0.0.4/go.mod h1:3waN4u02FiZivIV+p1y4d0Jo1jc6BViMA73C+sZo2fk= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs= github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -28,6 +31,11 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= diff --git a/main.go b/main.go index 78f427d..da2ca32 100644 --- a/main.go +++ b/main.go @@ -1,54 +1,7 @@ package main -import ( - "fmt" - "image" - "image/png" - "os" - - "github.com/cli/go-gh/v2/pkg/api" - "github.com/cli/go-gh/v2/pkg/repository" - "github.com/spenserblack/gh-collab-montage/pkg/avatar" - "github.com/spenserblack/gh-collab-montage/pkg/avatar/grid" - "github.com/spenserblack/gh-collab-montage/pkg/usersource" -) +import "github.com/spenserblack/gh-collab-montage/cmd" func main() { - f, err := os.Create("montage.png") - defer f.Close() - onError(err) - client, err := api.DefaultRESTClient() - onError(err) - repository, err := repository.Current() - onError(err) - source := usersource.NewContributors(client, repository.Owner, repository.Name) - avatars := []image.Image{} - for { - user, stop, err := source.Next() - onError(err) - if stop { - break - } - if user.Type != "User" { - continue - } - a, err := avatar.Decode(user.AvatarURL) - onError(err) - avatars = append(avatars, a) - } - g := grid.NewWithSize(len(avatars)) - for _, a := range avatars { - g.AddAvatar(a) - } - - m := g.Image() - err = png.Encode(f, m) - onError(err) -} - -func onError(err error) { - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } + cmd.Execute() } From b6672de288e228ff4bd5cf73d4a57a4d7785a1d8 Mon Sep 17 00:00:00 2001 From: Spenser Black Date: Wed, 22 Nov 2023 11:03:02 -0500 Subject: [PATCH 2/3] Add margins customizable margins between avatars --- cmd/root.go | 8 +++++++- pkg/avatar/grid/draw.go | 7 ++++--- pkg/avatar/grid/grid.go | 35 ++++++++++++++++++++++++++--------- pkg/avatar/grid/grid_test.go | 6 +++--- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index cc58f14..15afc11 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -39,7 +39,7 @@ var rootCmd = &cobra.Command{ onError(err) avatars = append(avatars, a) } - g := grid.NewWithSize(len(avatars)) + g := grid.NewWithSize(len(avatars), margin) for _, a := range avatars { g.AddAvatar(a) } @@ -49,3 +49,9 @@ var rootCmd = &cobra.Command{ onError(err) }, } + +var margin int + +func init() { + rootCmd.PersistentFlags().IntVarP(&margin, "margin", "m", 100, "Margin between avatars") +} diff --git a/pkg/avatar/grid/draw.go b/pkg/avatar/grid/draw.go index 59d8bf6..842550d 100644 --- a/pkg/avatar/grid/draw.go +++ b/pkg/avatar/grid/draw.go @@ -3,6 +3,7 @@ package grid import ( "image" "image/draw" + av "github.com/spenserblack/gh-collab-montage/pkg/avatar" ) @@ -17,11 +18,11 @@ func (g *AvatarGrid) AddAvatar(avatar image.Image) { } else if g.col == 0 && g.Rows() <= g.row { g.setBounds(g.Rows()+1, g.Cols()) } - x := g.col * av.Width - y := g.row * av.Height + x := (g.col * av.Width) + (g.col * g.margin) + y := (g.row * av.Height) + (g.row * g.margin) draw.Draw( g.image, - image.Rect(x, y, x + av.Width, y+av.Height), + image.Rect(x, y, x+av.Width, y+av.Height), avatar, image.Point{}, draw.Src, diff --git a/pkg/avatar/grid/grid.go b/pkg/avatar/grid/grid.go index 7f1fea3..93c5265 100644 --- a/pkg/avatar/grid/grid.go +++ b/pkg/avatar/grid/grid.go @@ -16,17 +16,25 @@ const perRow = 10 // It expands and adds new rows when needed. type AvatarGrid struct { image draw.Image - row int - col int + // Row is the current row (0-indexed). + row int + // Col is the current column (0-indexed). + col int + // Margin is the number of pixels between avatars. + margin int + // Cols is the number of columns in the grid. + cols int + // Rows is the number of rows in the grid. + rows int } // New returns a new AvatarGrid. -func New() *AvatarGrid { - return NewWithSize(0) +func New(margin int) *AvatarGrid { + return NewWithSize(0, margin) } // NewWithSize returns a new AvatarGrid with the given size. -func NewWithSize(avatars int) *AvatarGrid { +func NewWithSize(avatars int, margin int) *AvatarGrid { var cols, rows int if avatars == 0 { rows = 1 @@ -43,8 +51,13 @@ func NewWithSize(avatars int) *AvatarGrid { } else { cols = avatars } + width := cols*avatar.Width + (cols-1)*margin + height := rows*avatar.Height + (rows-1)*margin return &AvatarGrid{ - image: image.NewRGBA(image.Rect(0, 0, cols*avatar.Width, rows*avatar.Height)), + image: image.NewRGBA(image.Rect(0, 0, width, height)), + margin: margin, + cols: cols, + rows: rows, } } @@ -55,17 +68,21 @@ func (g AvatarGrid) Image() image.Image { // Cols returns the number of columns in the grid. func (g AvatarGrid) Cols() int { - return g.image.Bounds().Dx() / avatar.Width + return g.cols } // Rows returns the number of rows in the grid. func (g AvatarGrid) Rows() int { - return g.image.Bounds().Dy() / avatar.Height + return g.rows } // SetBounds changes the bounds of the underlying image. func (g *AvatarGrid) setBounds(rows, cols int) { - b := image.Rect(0, 0, cols*avatar.Width, rows*avatar.Height) + g.cols = cols + g.rows = rows + width := (cols * avatar.Width) + ((cols - 1) * g.margin) + height := (rows * avatar.Height) + ((rows - 1) * g.margin) + b := image.Rect(0, 0, width, height) newImage := image.NewRGBA(b) draw.Draw(newImage, b, g.image, image.Point{}, draw.Src) g.image = newImage diff --git a/pkg/avatar/grid/grid_test.go b/pkg/avatar/grid/grid_test.go index e5d2ef0..333fb5e 100644 --- a/pkg/avatar/grid/grid_test.go +++ b/pkg/avatar/grid/grid_test.go @@ -7,7 +7,7 @@ import ( ) func TestNew(t *testing.T) { - g := New() + g := New(100) if g.Cols() != 0 { t.Errorf("g.Cols() = %d, want 0", g.Cols()) } @@ -57,7 +57,7 @@ func TestNewWithSize(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - g := NewWithSize(tt.avatars) + g := NewWithSize(tt.avatars, 100) if g.Cols() != tt.cols { t.Errorf("g.Cols() = %d, want %d", g.Cols(), tt.cols) } @@ -172,7 +172,7 @@ func TestGrid_AddAvatar(t *testing.T) { for _, tt := range tests { t.Run(fmt.Sprintf("%d avatars added to %d-avatar grid", tt.n, tt.size), func(t *testing.T) { - g := NewWithSize(tt.size) + g := NewWithSize(tt.size, 100) for i := 0; i < tt.n; i++ { g.AddAvatar(avatar) } From 2a2221a1aec4563caea384a35d2768b22d19d709 Mon Sep 17 00:00:00 2001 From: Spenser Black Date: Wed, 22 Nov 2023 11:19:30 -0500 Subject: [PATCH 3/3] Refactor destination image generation This de-duplicates the logic for creating a background logic by putting it in its own function --- pkg/avatar/grid/grid.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/pkg/avatar/grid/grid.go b/pkg/avatar/grid/grid.go index 93c5265..4de22b7 100644 --- a/pkg/avatar/grid/grid.go +++ b/pkg/avatar/grid/grid.go @@ -51,14 +51,13 @@ func NewWithSize(avatars int, margin int) *AvatarGrid { } else { cols = avatars } - width := cols*avatar.Width + (cols-1)*margin - height := rows*avatar.Height + (rows-1)*margin - return &AvatarGrid{ - image: image.NewRGBA(image.Rect(0, 0, width, height)), + g := &AvatarGrid{ margin: margin, cols: cols, rows: rows, } + g.image = g.newDst() + return g } // Image returns the image of the grid. @@ -80,10 +79,14 @@ func (g AvatarGrid) Rows() int { func (g *AvatarGrid) setBounds(rows, cols int) { g.cols = cols g.rows = rows - width := (cols * avatar.Width) + ((cols - 1) * g.margin) - height := (rows * avatar.Height) + ((rows - 1) * g.margin) - b := image.Rect(0, 0, width, height) - newImage := image.NewRGBA(b) - draw.Draw(newImage, b, g.image, image.Point{}, draw.Src) + newImage := g.newDst() + draw.Draw(newImage, newImage.Bounds(), g.image, image.Point{}, draw.Src) g.image = newImage } + +// NewDst creates a new destination image based on the grid's dimensions. +func (g AvatarGrid) newDst() draw.Image { + width := g.cols*avatar.Width + (g.cols-1)*g.margin + height := g.rows*avatar.Height + (g.rows-1)*g.margin + return image.NewRGBA(image.Rect(0, 0, width, height)) +}