forked from go/golangs_learn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
249 lines
5.4 KiB
249 lines
5.4 KiB
3 years ago
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||
|
|
||
|
package main
|
||
|
|
||
|
// A Gio program that displays Go contributors from GitHub. See https://gioui.org for more information.
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"flag"
|
||
|
"fmt"
|
||
|
"image"
|
||
|
"log"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
|
||
|
"golang.org/x/oauth2"
|
||
|
|
||
|
"gioui.org/app"
|
||
|
"gioui.org/gesture"
|
||
|
"gioui.org/io/key"
|
||
|
"gioui.org/io/system"
|
||
|
"gioui.org/layout"
|
||
|
"gioui.org/op"
|
||
|
"gioui.org/unit"
|
||
|
|
||
|
"github.com/google/go-github/v24/github"
|
||
|
|
||
|
_ "image/jpeg"
|
||
|
_ "image/png"
|
||
|
|
||
|
_ "net/http/pprof"
|
||
|
)
|
||
|
|
||
|
type App struct {
|
||
|
w *app.Window
|
||
|
|
||
|
ui *UI
|
||
|
|
||
|
updateUsers chan []*user
|
||
|
commitsResult chan []*github.Commit
|
||
|
ctx context.Context
|
||
|
ctxCancel context.CancelFunc
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
prof = flag.Bool("profile", false, "serve profiling data at http://localhost:6060")
|
||
|
stats = flag.Bool("stats", false, "show rendering statistics")
|
||
|
token = flag.String("token", "", "Github authentication token")
|
||
|
)
|
||
|
|
||
|
func main() {
|
||
|
flag.Parse()
|
||
|
initProfiling()
|
||
|
if *token == "" {
|
||
|
fmt.Println("The quota for anonymous GitHub API access is very low. Specify a token with -token to avoid quota errors.")
|
||
|
fmt.Println("See https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line.")
|
||
|
}
|
||
|
go func() {
|
||
|
w := app.NewWindow(
|
||
|
app.Size(unit.Dp(400), unit.Dp(800)),
|
||
|
app.Title("Gophers"),
|
||
|
)
|
||
|
if err := newApp(w).run(); err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
os.Exit(0)
|
||
|
}()
|
||
|
app.Main()
|
||
|
}
|
||
|
|
||
|
func initProfiling() {
|
||
|
if !*prof {
|
||
|
return
|
||
|
}
|
||
|
go func() {
|
||
|
log.Println(http.ListenAndServe("localhost:6060", nil))
|
||
|
}()
|
||
|
}
|
||
|
|
||
|
func (a *App) run() error {
|
||
|
a.ui.profiling = *stats
|
||
|
var ops op.Ops
|
||
|
for {
|
||
|
select {
|
||
|
case users := <-a.updateUsers:
|
||
|
a.ui.users = users
|
||
|
a.ui.userClicks = make([]gesture.Click, len(users))
|
||
|
a.w.Invalidate()
|
||
|
case commits := <-a.commitsResult:
|
||
|
a.ui.selectedUser.commits = commits
|
||
|
a.w.Invalidate()
|
||
|
case e := <-a.w.Events():
|
||
|
switch e := e.(type) {
|
||
|
case key.Event:
|
||
|
switch e.Name {
|
||
|
case key.NameEscape:
|
||
|
os.Exit(0)
|
||
|
case "P":
|
||
|
if e.Modifiers.Contain(key.ModShortcut) {
|
||
|
a.ui.profiling = !a.ui.profiling
|
||
|
a.w.Invalidate()
|
||
|
}
|
||
|
}
|
||
|
case system.DestroyEvent:
|
||
|
return e.Err
|
||
|
case system.StageEvent:
|
||
|
if e.Stage >= system.StageRunning {
|
||
|
if a.ctxCancel == nil {
|
||
|
a.ctx, a.ctxCancel = context.WithCancel(context.Background())
|
||
|
}
|
||
|
if a.ui.users == nil {
|
||
|
go a.fetchContributors()
|
||
|
}
|
||
|
} else {
|
||
|
if a.ctxCancel != nil {
|
||
|
a.ctxCancel()
|
||
|
a.ctxCancel = nil
|
||
|
}
|
||
|
}
|
||
|
case *system.CommandEvent:
|
||
|
switch e.Type {
|
||
|
case system.CommandBack:
|
||
|
if a.ui.selectedUser != nil {
|
||
|
a.ui.selectedUser = nil
|
||
|
e.Cancel = true
|
||
|
a.w.Invalidate()
|
||
|
}
|
||
|
}
|
||
|
case system.FrameEvent:
|
||
|
gtx := layout.NewContext(&ops, e)
|
||
|
a.ui.Layout(gtx)
|
||
|
e.Frame(gtx.Ops)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func newApp(w *app.Window) *App {
|
||
|
a := &App{
|
||
|
w: w,
|
||
|
updateUsers: make(chan []*user),
|
||
|
commitsResult: make(chan []*github.Commit, 1),
|
||
|
}
|
||
|
fetch := func(u string) {
|
||
|
a.fetchCommits(a.ctx, u)
|
||
|
}
|
||
|
a.ui = newUI(fetch)
|
||
|
return a
|
||
|
}
|
||
|
|
||
|
func githubClient(ctx context.Context) *github.Client {
|
||
|
var tc *http.Client
|
||
|
if *token != "" {
|
||
|
ts := oauth2.StaticTokenSource(
|
||
|
&oauth2.Token{AccessToken: *token},
|
||
|
)
|
||
|
tc = oauth2.NewClient(ctx, ts)
|
||
|
}
|
||
|
return github.NewClient(tc)
|
||
|
}
|
||
|
|
||
|
func (a *App) fetchContributors() {
|
||
|
client := githubClient(a.ctx)
|
||
|
cons, _, err := client.Repositories.ListContributors(a.ctx, "golang", "go", nil)
|
||
|
if err != nil {
|
||
|
fmt.Fprintf(os.Stderr, "github: failed to fetch contributors: %v\n", err)
|
||
|
return
|
||
|
}
|
||
|
var users []*user
|
||
|
userErrs := make(chan error, len(cons))
|
||
|
avatarErrs := make(chan error, len(cons))
|
||
|
for _, con := range cons {
|
||
|
con := con
|
||
|
avatar := con.GetAvatarURL()
|
||
|
if avatar == "" {
|
||
|
continue
|
||
|
}
|
||
|
u := &user{
|
||
|
login: con.GetLogin(),
|
||
|
}
|
||
|
users = append(users, u)
|
||
|
go func() {
|
||
|
guser, _, err := client.Users.Get(a.ctx, u.login)
|
||
|
if err != nil {
|
||
|
avatarErrs <- err
|
||
|
return
|
||
|
}
|
||
|
u.name = guser.GetName()
|
||
|
u.company = guser.GetCompany()
|
||
|
avatarErrs <- nil
|
||
|
}()
|
||
|
go func() {
|
||
|
a, err := fetchImage(avatar)
|
||
|
if a != nil {
|
||
|
u.avatar = a
|
||
|
}
|
||
|
userErrs <- err
|
||
|
}()
|
||
|
}
|
||
|
for i := 0; i < len(cons); i++ {
|
||
|
if err := <-userErrs; err != nil {
|
||
|
fmt.Fprintf(os.Stderr, "github: failed to fetch user: %v\n", err)
|
||
|
}
|
||
|
if err := <-avatarErrs; err != nil {
|
||
|
fmt.Fprintf(os.Stderr, "github: failed to fetch avatar: %v\n", err)
|
||
|
}
|
||
|
}
|
||
|
// Drop users with no avatar or name.
|
||
|
for i := len(users) - 1; i >= 0; i-- {
|
||
|
if u := users[i]; u.name == "" || u.avatar.Bounds().Size() == (image.Point{}) {
|
||
|
users = append(users[:i], users[i+1:]...)
|
||
|
}
|
||
|
}
|
||
|
a.updateUsers <- users
|
||
|
}
|
||
|
|
||
|
func fetchImage(url string) (image.Image, error) {
|
||
|
resp, err := http.Get(url)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("fetchImage: http.Get(%q): %v", url, err)
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
img, _, err := image.Decode(resp.Body)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("fetchImage: image decode failed: %v", err)
|
||
|
}
|
||
|
return img, nil
|
||
|
}
|
||
|
|
||
|
func (a *App) fetchCommits(ctx context.Context, user string) {
|
||
|
go func() {
|
||
|
gh := githubClient(ctx)
|
||
|
repoCommits, _, err := gh.Repositories.ListCommits(ctx, "golang", "go", &github.CommitsListOptions{
|
||
|
Author: user,
|
||
|
})
|
||
|
if err != nil {
|
||
|
log.Printf("failed to fetch commits: %v", err)
|
||
|
return
|
||
|
}
|
||
|
var commits []*github.Commit
|
||
|
for _, commit := range repoCommits {
|
||
|
if c := commit.GetCommit(); c != nil {
|
||
|
commits = append(commits, c)
|
||
|
}
|
||
|
}
|
||
|
a.commitsResult <- commits
|
||
|
}()
|
||
|
}
|