1package credits
2
3import (
4	"bytes"
5	"fmt"
6	"math"
7	"math/rand"
8	"net/http"
9	"os"
10	"os/exec"
11	"runtime"
12	"strings"
13	"time"
14
15	"github.com/MakeNowJust/heredoc"
16	"github.com/cli/cli/v2/api"
17	"github.com/cli/cli/v2/internal/ghrepo"
18	"github.com/cli/cli/v2/pkg/cmdutil"
19	"github.com/cli/cli/v2/pkg/iostreams"
20	"github.com/cli/cli/v2/utils"
21	"github.com/spf13/cobra"
22)
23
24type CreditsOptions struct {
25	HttpClient func() (*http.Client, error)
26	BaseRepo   func() (ghrepo.Interface, error)
27	IO         *iostreams.IOStreams
28
29	Repository string
30	Static     bool
31}
32
33func NewCmdCredits(f *cmdutil.Factory, runF func(*CreditsOptions) error) *cobra.Command {
34	opts := &CreditsOptions{
35		HttpClient: f.HttpClient,
36		IO:         f.IOStreams,
37		BaseRepo:   f.BaseRepo,
38		Repository: "cli/cli",
39	}
40
41	cmd := &cobra.Command{
42		Use:   "credits",
43		Short: "View credits for this tool",
44		Long:  `View animated credits for gh, the tool you are currently using :)`,
45		Example: heredoc.Doc(`
46			# see a credits animation for this project
47			$ gh credits
48
49			# display a non-animated thank you
50			$ gh credits -s
51
52			# just print the contributors, one per line
53			$ gh credits | cat
54		`),
55		Args: cobra.ExactArgs(0),
56		RunE: func(cmd *cobra.Command, args []string) error {
57			if runF != nil {
58				return runF(opts)
59			}
60
61			return creditsRun(opts)
62		},
63		Hidden: true,
64	}
65
66	cmd.Flags().BoolVarP(&opts.Static, "static", "s", false, "Print a static version of the credits")
67
68	return cmd
69}
70
71func NewCmdRepoCredits(f *cmdutil.Factory, runF func(*CreditsOptions) error) *cobra.Command {
72	opts := &CreditsOptions{
73		HttpClient: f.HttpClient,
74		BaseRepo:   f.BaseRepo,
75		IO:         f.IOStreams,
76	}
77
78	cmd := &cobra.Command{
79		Use:   "credits [<repository>]",
80		Short: "View credits for a repository",
81		Example: heredoc.Doc(`
82      # view credits for the current repository
83      $ gh repo credits
84
85      # view credits for a specific repository
86      $ gh repo credits cool/repo
87
88      # print a non-animated thank you
89      $ gh repo credits -s
90
91      # pipe to just print the contributors, one per line
92      $ gh repo credits | cat
93    `),
94		Args: cobra.MaximumNArgs(1),
95		RunE: func(cmd *cobra.Command, args []string) error {
96			if len(args) > 0 {
97				opts.Repository = args[0]
98			}
99
100			if runF != nil {
101				return runF(opts)
102			}
103
104			return creditsRun(opts)
105		},
106		Hidden: true,
107	}
108
109	cmd.Flags().BoolVarP(&opts.Static, "static", "s", false, "Print a static version of the credits")
110
111	return cmd
112}
113
114func creditsRun(opts *CreditsOptions) error {
115	isWindows := runtime.GOOS == "windows"
116	httpClient, err := opts.HttpClient()
117	if err != nil {
118		return err
119	}
120
121	client := api.NewClientFromHTTP(httpClient)
122
123	var baseRepo ghrepo.Interface
124	if opts.Repository == "" {
125		baseRepo, err = opts.BaseRepo()
126		if err != nil {
127			return err
128		}
129	} else {
130		baseRepo, err = ghrepo.FromFullName(opts.Repository)
131		if err != nil {
132			return err
133		}
134	}
135
136	type Contributor struct {
137		Login string
138		Type  string
139	}
140
141	type Result []Contributor
142
143	result := Result{}
144	body := bytes.NewBufferString("")
145	path := fmt.Sprintf("repos/%s/%s/contributors", baseRepo.RepoOwner(), baseRepo.RepoName())
146
147	err = client.REST(baseRepo.RepoHost(), "GET", path, body, &result)
148	if err != nil {
149		return err
150	}
151
152	isTTY := opts.IO.IsStdoutTTY()
153
154	static := opts.Static || isWindows
155
156	out := opts.IO.Out
157	cs := opts.IO.ColorScheme()
158
159	if isTTY && static {
160		fmt.Fprintln(out, "THANK YOU CONTRIBUTORS!!! <3")
161		fmt.Fprintln(out, "")
162	}
163
164	logins := []string{}
165	for x, c := range result {
166		if c.Type != "User" {
167			continue
168		}
169
170		if isTTY && !static {
171			logins = append(logins, cs.ColorFromString(getColor(x))(c.Login))
172		} else {
173			fmt.Fprintf(out, "%s\n", c.Login)
174		}
175	}
176
177	if !isTTY || static {
178		return nil
179	}
180
181	rand.Seed(time.Now().UnixNano())
182
183	lines := []string{}
184
185	thankLines := strings.Split(thankYou, "\n")
186	for x, tl := range thankLines {
187		lines = append(lines, cs.ColorFromString(getColor(x))(tl))
188	}
189	lines = append(lines, "")
190	lines = append(lines, logins...)
191	lines = append(lines, "( <3 press ctrl-c to quit <3 )")
192
193	termWidth, termHeight, err := utils.TerminalSize(out)
194	if err != nil {
195		return err
196	}
197
198	margin := termWidth / 3
199
200	starLinesLeft := []string{}
201	for x := 0; x < len(lines); x++ {
202		starLinesLeft = append(starLinesLeft, starLine(margin))
203	}
204
205	starLinesRight := []string{}
206	for x := 0; x < len(lines); x++ {
207		lineWidth := termWidth - (margin + len(lines[x]))
208		starLinesRight = append(starLinesRight, starLine(lineWidth))
209	}
210
211	loop := true
212	startx := termHeight - 1
213	li := 0
214
215	for loop {
216		clear()
217		for x := 0; x < termHeight; x++ {
218			if x == startx || startx < 0 {
219				starty := 0
220				if startx < 0 {
221					starty = int(math.Abs(float64(startx)))
222				}
223				for y := starty; y < li+1; y++ {
224					if y >= len(lines) {
225						continue
226					}
227					starLineLeft := starLinesLeft[y]
228					starLinesLeft[y] = twinkle(starLineLeft)
229					starLineRight := starLinesRight[y]
230					starLinesRight[y] = twinkle(starLineRight)
231					fmt.Fprintf(out, "%s %s %s\n", starLineLeft, lines[y], starLineRight)
232				}
233				li += 1
234				x += li
235			} else {
236				fmt.Fprintf(out, "\n")
237			}
238		}
239		if li < len(lines) {
240			startx -= 1
241		}
242		time.Sleep(300 * time.Millisecond)
243	}
244
245	return nil
246}
247
248func starLine(width int) string {
249	line := ""
250	starChance := 0.1
251	for y := 0; y < width; y++ {
252		chance := rand.Float64()
253		if chance <= starChance {
254			charRoll := rand.Float64()
255			switch {
256			case charRoll < 0.3:
257				line += "."
258			case charRoll > 0.3 && charRoll < 0.6:
259				line += "+"
260			default:
261				line += "*"
262			}
263		} else {
264			line += " "
265		}
266	}
267
268	return line
269}
270
271func twinkle(starLine string) string {
272	starLine = strings.ReplaceAll(starLine, ".", "P")
273	starLine = strings.ReplaceAll(starLine, "+", "A")
274	starLine = strings.ReplaceAll(starLine, "*", ".")
275	starLine = strings.ReplaceAll(starLine, "P", "+")
276	starLine = strings.ReplaceAll(starLine, "A", "*")
277	return starLine
278}
279
280func getColor(x int) string {
281	rainbow := []string{
282		"magenta",
283		"red",
284		"yellow",
285		"green",
286		"cyan",
287		"blue",
288	}
289
290	ix := x % len(rainbow)
291
292	return rainbow[ix]
293}
294
295func clear() {
296	// on windows we'd do cmd := exec.Command("cmd", "/c", "cls"); unfortunately the draw speed is so
297	// slow that the animation is very jerky, flashy, and painful to look at.
298	cmd := exec.Command("clear")
299	cmd.Stdout = os.Stdout
300	_ = cmd.Run()
301}
302
303var thankYou = `
304     _                    _
305    | |                  | |
306_|_ | |     __,   _  _   | |           __
307 |  |/ \   /  |  / |/ |  |/_)   |   | /  \_|   |
308 |_/|   |_/\_/|_/  |  |_/| \_/   \_/|/\__/  \_/|_/
309                                   /|
310                                   \|
311                              _
312                           o | |                           |
313 __   __   _  _  _|_  ,_     | |        _|_  __   ,_    ,  |
314/    /  \_/ |/ |  |  /  |  | |/ \_|   |  |  /  \_/  |  / \_|
315\___/\__/   |  |_/|_/   |_/|_/\_/  \_/|_/|_/\__/    |_/ \/ o
316
317
318`
319