mirror of
https://github.com/tiennm99/ghglance.git
synced 2026-10-20 00:15:14 +00:00
feat: implement profile summary cards with GraphQL fetch and Action wrapper
- Add GraphQL client fetching profile, stats, language aggregation, and per-repo commit histograms for the productive-time heatmap. - Render real SVG cards (profile details, top languages, stats grid, weekday×hour heatmap) with XML escaping and thousands-formatted numbers. - Expand theme palette to 30 built-ins ported from github-readme-stats; add -list-themes, multi-theme rendering, and 'all' shortcut. - Package as Docker-based GitHub Action (action.yml, Dockerfile, entrypoint.sh) with optional auto-commit of generated cards. - Release workflow publishes GHCR image and cross-platform binaries on v* tags. - Unit tests cover rendering, XML escape, number formatting, language sort.
This commit is contained in:
1 parent
a91d964a1d
commit
ff4975fae2
19 files changed
+1312
-135
No files matched your search
@@ -5,6 +5,8 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tiennm99/ghstats/internal/card"
|
||||
"github.com/tiennm99/ghstats/internal/github"
|
||||
@@ -13,36 +15,97 @@ import (
|
||||
|
||||
func main() {
|
||||
var (
|
||||
user = flag.String("user", "", "GitHub username (required)")
|
||||
token = flag.String("token", os.Getenv("GITHUB_TOKEN"), "GitHub token (or env GITHUB_TOKEN)")
|
||||
out = flag.String("out", "output", "output directory for SVG cards")
|
||||
themeID = flag.String("theme", "dracula", "theme id (dracula, default, github)")
|
||||
user = flag.String("user", "", "GitHub username (required)")
|
||||
token = flag.String("token", os.Getenv("GITHUB_TOKEN"), "GitHub token (or env GITHUB_TOKEN)")
|
||||
out = flag.String("out", "output", "output directory")
|
||||
themesFlag = flag.String("themes", "dracula", "comma-separated theme ids, or 'all'")
|
||||
tzName = flag.String("tz", "Local", "timezone for productive-time card (IANA name, e.g. Asia/Saigon)")
|
||||
topRepos = flag.Int("top-repos", 10, "owned repos to sample for productive-time heatmap (0 to skip)")
|
||||
perRepo = flag.Int("commits-per-repo", 100, "max commits sampled per repo")
|
||||
listThemes = flag.Bool("list-themes", false, "print available theme ids and exit")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *listThemes {
|
||||
for _, id := range theme.IDs() {
|
||||
fmt.Println(id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if *user == "" {
|
||||
fmt.Fprintln(os.Stderr, "error: -user is required")
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
th, ok := theme.Lookup(*themeID)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "error: unknown theme %q\n", *themeID)
|
||||
selected, err := resolveThemes(*themesFlag)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
loc, err := time.LoadLocation(*tzName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warn: unknown timezone %q, falling back to UTC\n", *tzName)
|
||||
loc = time.UTC
|
||||
}
|
||||
|
||||
client := github.NewClient(*token)
|
||||
profile, err := client.Profile(*user)
|
||||
profile, err := client.FetchProfile(*user)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: fetch profile: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := card.RenderAll(profile, th, *out); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: render cards: %v\n", err)
|
||||
os.Exit(1)
|
||||
if *topRepos > 0 && profile.ID != "" {
|
||||
repos := profile.TopRepos
|
||||
if len(repos) > *topRepos {
|
||||
repos = repos[:*topRepos]
|
||||
}
|
||||
if err := client.FetchProductive(profile, repos, loc, *perRepo); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warn: productive-time fetch: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("wrote cards to %s/%s/\n", *out, th.ID)
|
||||
for _, t := range selected {
|
||||
if err := card.RenderAll(profile, t, *out); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: render %s: %v\n", t.ID, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("wrote %s/%s/\n", *out, t.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveThemes(spec string) ([]theme.Theme, error) {
|
||||
spec = strings.TrimSpace(spec)
|
||||
if spec == "" {
|
||||
return nil, fmt.Errorf("no themes specified")
|
||||
}
|
||||
if spec == "all" {
|
||||
ids := theme.IDs()
|
||||
out := make([]theme.Theme, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if t, ok := theme.Lookup(id); ok {
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
var out []theme.Theme
|
||||
for _, id := range strings.Split(spec, ",") {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
t, ok := theme.Lookup(id)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown theme %q (use -list-themes)", id)
|
||||
}
|
||||
out = append(out, t)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, fmt.Errorf("no valid themes")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in new issue
Block a user