diff --git a/.gitignore b/.gitignore index 3ef7b65..005c643 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .claude .serena +.mcp.json commands.nu /gh-oss-stats scripts/ diff --git a/README.md b/README.md index 14866c6..6f9a784 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Track and display your merged PRs, commits, and contributions to external reposi - πŸ” **External Contribution Tracking** - Discovers all your merged PRs to repos you don't own - πŸ“Š **Comprehensive Stats** - Total PRs, commits, lines of code, and repository stars - ⭐ **Smart Filtering** - Filter by minimum stars, exclude organizations -- 🎭 18 **Color Themes** - Match your profile's aesthetic +- 🎭 **18 Color Themes** - Match your profile's aesthetic - πŸ“¦ **Developer-Friendly** - Use as a Go library or standalone CLI, outputs JSON @@ -72,9 +72,9 @@ jobs: ### Run it manually If you don't want to wait for the end of the week, you can run this action manually -1. G to `your github repo` +1. Go to your github repo 2. Click `actions` tab -3. On the left side, you shouls see your action named `Generate OSS Badge`, click on it +3. On the left side, you should see your action named `Generate OSS Badge`, click on it 4. Click on `run workflow` drop down, then click `run workflow` That is it. It will run and generate the badge for you. diff --git a/cmd/gh-oss-stats/badgeConfig.go b/cmd/gh-oss-stats/badgeConfig.go index a5bf0b9..d6fda04 100644 --- a/cmd/gh-oss-stats/badgeConfig.go +++ b/cmd/gh-oss-stats/badgeConfig.go @@ -13,6 +13,16 @@ type BadgeConfig struct { output string sort string limit int + // Custom color overrides (empty = use theme default) + colorBackground string + colorBackgroundAlt string + colorText string + colorTextSecondary string + colorBorder string + colorAccent string + colorPositive string + colorNegative string + colorStar string } // newBadgeConfig creates a new BadgeConfig with default values @@ -34,4 +44,14 @@ func (bf *BadgeConfig) registerBadgeFlags(fs *flag.FlagSet) { fs.StringVar(&bf.output, "badge-output", "", "Badge output file (default: badge.svg)") fs.StringVar(&bf.sort, "badge-sort", string(badge.DefaultSortBy), "Sort contributions by: prs, stars, commits") fs.IntVar(&bf.limit, "badge-limit", badge.DefaultPRsLimit, "Number of contributions to show") + + fs.StringVar(&bf.colorBackground, "badge-color-background", "", "Custom background color (hex, e.g. #1a1b26)") + fs.StringVar(&bf.colorBackgroundAlt, "badge-color-background-alt", "", "Custom alt background color (hex)") + fs.StringVar(&bf.colorText, "badge-color-text", "", "Custom primary text color (hex)") + fs.StringVar(&bf.colorTextSecondary, "badge-color-text-secondary", "", "Custom secondary text color (hex)") + fs.StringVar(&bf.colorBorder, "badge-color-border", "", "Custom border color (hex)") + fs.StringVar(&bf.colorAccent, "badge-color-accent", "", "Custom accent/brand color (hex)") + fs.StringVar(&bf.colorPositive, "badge-color-positive", "", "Custom positive/additions color (hex)") + fs.StringVar(&bf.colorNegative, "badge-color-negative", "", "Custom negative/deletions color (hex)") + fs.StringVar(&bf.colorStar, "badge-color-star", "", "Custom star count color (hex)") } diff --git a/cmd/gh-oss-stats/badgeConfig_test.go b/cmd/gh-oss-stats/badgeConfig_test.go index 887cb2c..ebe077e 100644 --- a/cmd/gh-oss-stats/badgeConfig_test.go +++ b/cmd/gh-oss-stats/badgeConfig_test.go @@ -188,8 +188,115 @@ func TestBadgeConfigPointerReturn(t *testing.T) { // Modifying bc1 should not affect bc2 bc1.style = "modified" - + if bc2.style == "modified" { t.Error("Modifying bc1 affected bc2; they should be independent instances") } } + +func TestBadgeColorFlagsRegistered(t *testing.T) { + bc := newBadgeConfig() + fs := flag.NewFlagSet("test", flag.ContinueOnError) + bc.registerBadgeFlags(fs) + + colorFlags := []string{ + "badge-color-background", + "badge-color-background-alt", + "badge-color-text", + "badge-color-text-secondary", + "badge-color-border", + "badge-color-accent", + "badge-color-positive", + "badge-color-negative", + "badge-color-star", + } + + for _, name := range colorFlags { + t.Run(name, func(t *testing.T) { + f := fs.Lookup(name) + if f == nil { + t.Errorf("flag --%s not registered", name) + return + } + if f.DefValue != "" { + t.Errorf("flag --%s default = %q, want %q", name, f.DefValue, "") + } + }) + } +} + +func TestBadgeColorFlagDefaults(t *testing.T) { + bc := newBadgeConfig() + fs := flag.NewFlagSet("test", flag.ContinueOnError) + bc.registerBadgeFlags(fs) + + // Parse with no color flags β€” all color fields should remain empty + if err := fs.Parse([]string{}); err != nil { + t.Fatalf("Parse failed: %v", err) + } + + fields := []struct { + name string + got string + }{ + {"colorBackground", bc.colorBackground}, + {"colorBackgroundAlt", bc.colorBackgroundAlt}, + {"colorText", bc.colorText}, + {"colorTextSecondary", bc.colorTextSecondary}, + {"colorBorder", bc.colorBorder}, + {"colorAccent", bc.colorAccent}, + {"colorPositive", bc.colorPositive}, + {"colorNegative", bc.colorNegative}, + {"colorStar", bc.colorStar}, + } + + for _, f := range fields { + if f.got != "" { + t.Errorf("%s default = %q, want empty string", f.name, f.got) + } + } +} + +func TestBadgeColorFlagsParsed(t *testing.T) { + bc := newBadgeConfig() + fs := flag.NewFlagSet("test", flag.ContinueOnError) + bc.registerBadgeFlags(fs) + + args := []string{ + "--badge-color-background", "#0d1117", + "--badge-color-background-alt", "#161b22", + "--badge-color-text", "#c9d1d9", + "--badge-color-text-secondary", "#8b949e", + "--badge-color-border", "#30363d", + "--badge-color-accent", "#58a6ff", + "--badge-color-positive", "#3fb950", + "--badge-color-negative", "#f85149", + "--badge-color-star", "#e3b341", + } + + if err := fs.Parse(args); err != nil { + t.Fatalf("Parse failed: %v", err) + } + + tests := []struct { + name string + got string + want string + }{ + {"colorBackground", bc.colorBackground, "#0d1117"}, + {"colorBackgroundAlt", bc.colorBackgroundAlt, "#161b22"}, + {"colorText", bc.colorText, "#c9d1d9"}, + {"colorTextSecondary", bc.colorTextSecondary, "#8b949e"}, + {"colorBorder", bc.colorBorder, "#30363d"}, + {"colorAccent", bc.colorAccent, "#58a6ff"}, + {"colorPositive", bc.colorPositive, "#3fb950"}, + {"colorNegative", bc.colorNegative, "#f85149"}, + {"colorStar", bc.colorStar, "#e3b341"}, + } + + for _, tt := range tests { + if tt.got != tt.want { + t.Errorf("%s = %q, want %q", tt.name, tt.got, tt.want) + } + } +} diff --git a/cmd/gh-oss-stats/badgeGeneration.go b/cmd/gh-oss-stats/badgeGeneration.go index aebee13..1e953a7 100644 --- a/cmd/gh-oss-stats/badgeGeneration.go +++ b/cmd/gh-oss-stats/badgeGeneration.go @@ -3,11 +3,21 @@ package main import ( "fmt" "os" + "regexp" "github.com/mabd-dev/gh-oss-stats/pkg/ossstats" "github.com/mabd-dev/gh-oss-stats/pkg/ossstats/badge" ) +var hexColorRE = regexp.MustCompile(`^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$`) + +func validateHexColor(flag, value string) error { + if !hexColorRE.MatchString(value) { + return fmt.Errorf("invalid color for --%s: %q (expected hex format, e.g. #rgb, #rrggbb, #rrggbbaa)", flag, value) + } + return nil +} + func createBadgeOptions(conf BadgeConfig) (badge.BadgeOptions, error) { badgeStyle, err := badge.BadgeStyleFromName(conf.style) if err != nil { @@ -33,12 +43,53 @@ func createBadgeOptions(conf BadgeConfig) (badge.BadgeOptions, error) { os.Exit(1) } + colorFlags := []struct { + flag string + value string + }{ + {"badge-color-background", conf.colorBackground}, + {"badge-color-background-alt", conf.colorBackgroundAlt}, + {"badge-color-text", conf.colorText}, + {"badge-color-text-secondary", conf.colorTextSecondary}, + {"badge-color-border", conf.colorBorder}, + {"badge-color-accent", conf.colorAccent}, + {"badge-color-positive", conf.colorPositive}, + {"badge-color-negative", conf.colorNegative}, + {"badge-color-star", conf.colorStar}, + } + for _, cf := range colorFlags { + if cf.value != "" { + if err := validateHexColor(cf.flag, cf.value); err != nil { + return badge.BadgeOptions{}, err + } + } + } + + var customColors *badge.ThemeColors + if conf.colorBackground != "" || conf.colorBackgroundAlt != "" || + conf.colorText != "" || conf.colorTextSecondary != "" || + conf.colorBorder != "" || conf.colorAccent != "" || + conf.colorPositive != "" || conf.colorNegative != "" || conf.colorStar != "" { + customColors = &badge.ThemeColors{ + Background: conf.colorBackground, + BackgroundAlt: conf.colorBackgroundAlt, + Text: conf.colorText, + TextSecondary: conf.colorTextSecondary, + Border: conf.colorBorder, + Accent: conf.colorAccent, + Positive: conf.colorPositive, + Negative: conf.colorNegative, + Star: conf.colorStar, + } + } + return badge.BadgeOptions{ - Style: badgeStyle, - Variant: badgeVariant, - Theme: badgeTheme, - SortBy: badgeSortBy, - Limit: conf.limit, + Style: badgeStyle, + Variant: badgeVariant, + Theme: badgeTheme, + SortBy: badgeSortBy, + Limit: conf.limit, + CustomColors: customColors, }, nil } diff --git a/cmd/gh-oss-stats/badgeGeneration_test.go b/cmd/gh-oss-stats/badgeGeneration_test.go new file mode 100644 index 0000000..454997d --- /dev/null +++ b/cmd/gh-oss-stats/badgeGeneration_test.go @@ -0,0 +1,334 @@ +package main + +import ( + "testing" + + "github.com/mabd-dev/gh-oss-stats/pkg/ossstats/badge" +) + +func TestCreateBadgeOptionsNoCustomColors(t *testing.T) { + conf := BadgeConfig{ + style: string(badge.DefaultBadgeStyle), + variant: string(badge.DefaultBadgeVariant), + theme: string(badge.DefaultBadgeTheme), + sort: string(badge.DefaultSortBy), + limit: badge.DefaultPRsLimit, + // all color fields left as zero value ("") + } + + opts, err := createBadgeOptions(conf) + if err != nil { + t.Fatalf("createBadgeOptions failed: %v", err) + } + + if opts.CustomColors != nil { + t.Errorf("CustomColors = %+v, want nil when no color flags are set", opts.CustomColors) + } +} + +func TestCreateBadgeOptionsAllCustomColors(t *testing.T) { + conf := BadgeConfig{ + style: string(badge.DefaultBadgeStyle), + variant: string(badge.DefaultBadgeVariant), + theme: string(badge.DefaultBadgeTheme), + sort: string(badge.DefaultSortBy), + limit: badge.DefaultPRsLimit, + colorBackground: "#0d1117", + colorBackgroundAlt: "#161b22", + colorText: "#c9d1d9", + colorTextSecondary: "#8b949e", + colorBorder: "#30363d", + colorAccent: "#58a6ff", + colorPositive: "#3fb950", + colorNegative: "#f85149", + colorStar: "#e3b341", + } + + opts, err := createBadgeOptions(conf) + if err != nil { + t.Fatalf("createBadgeOptions failed: %v", err) + } + + if opts.CustomColors == nil { + t.Fatal("CustomColors is nil, want non-nil when color flags are set") + } + + c := opts.CustomColors + tests := []struct { + name string + got string + want string + }{ + {"Background", c.Background, "#0d1117"}, + {"BackgroundAlt", c.BackgroundAlt, "#161b22"}, + {"Text", c.Text, "#c9d1d9"}, + {"TextSecondary", c.TextSecondary, "#8b949e"}, + {"Border", c.Border, "#30363d"}, + {"Accent", c.Accent, "#58a6ff"}, + {"Positive", c.Positive, "#3fb950"}, + {"Negative", c.Negative, "#f85149"}, + {"Star", c.Star, "#e3b341"}, + } + + for _, tt := range tests { + if tt.got != tt.want { + t.Errorf("CustomColors.%s = %q, want %q", tt.name, tt.got, tt.want) + } + } +} + +func TestCreateBadgeOptionsPartialCustomColors(t *testing.T) { + // Only accent and star are set; all others should be empty strings in CustomColors. + conf := BadgeConfig{ + style: string(badge.DefaultBadgeStyle), + variant: string(badge.DefaultBadgeVariant), + theme: string(badge.DefaultBadgeTheme), + sort: string(badge.DefaultSortBy), + limit: badge.DefaultPRsLimit, + colorAccent: "#ff6b6b", + colorStar: "#ffd700", + } + + opts, err := createBadgeOptions(conf) + if err != nil { + t.Fatalf("createBadgeOptions failed: %v", err) + } + + if opts.CustomColors == nil { + t.Fatal("CustomColors is nil, want non-nil when at least one color flag is set") + } + + if opts.CustomColors.Accent != "#ff6b6b" { + t.Errorf("Accent = %q, want %q", opts.CustomColors.Accent, "#ff6b6b") + } + if opts.CustomColors.Star != "#ffd700" { + t.Errorf("Star = %q, want %q", opts.CustomColors.Star, "#ffd700") + } + // Unset fields should be empty strings (theme override logic in RenderSVG will skip them). + if opts.CustomColors.Background != "" { + t.Errorf("Background = %q, want empty string for unset flag", opts.CustomColors.Background) + } +} + +func TestCreateBadgeOptionsSingleColorFlag(t *testing.T) { + colorFields := []struct { + name string + makeConf func() BadgeConfig + check func(*badge.ThemeColors) (string, string) // returns got, want + }{ + { + "background", + func() BadgeConfig { + c := baseConf() + c.colorBackground = "#aabbcc" + return c + }, + func(c *badge.ThemeColors) (string, string) { return c.Background, "#aabbcc" }, + }, + { + "background-alt", + func() BadgeConfig { + c := baseConf() + c.colorBackgroundAlt = "#112233" + return c + }, + func(c *badge.ThemeColors) (string, string) { return c.BackgroundAlt, "#112233" }, + }, + { + "text", + func() BadgeConfig { + c := baseConf() + c.colorText = "#ffffff" + return c + }, + func(c *badge.ThemeColors) (string, string) { return c.Text, "#ffffff" }, + }, + { + "text-secondary", + func() BadgeConfig { + c := baseConf() + c.colorTextSecondary = "#aaaaaa" + return c + }, + func(c *badge.ThemeColors) (string, string) { return c.TextSecondary, "#aaaaaa" }, + }, + { + "border", + func() BadgeConfig { + c := baseConf() + c.colorBorder = "#333333" + return c + }, + func(c *badge.ThemeColors) (string, string) { return c.Border, "#333333" }, + }, + { + "accent", + func() BadgeConfig { + c := baseConf() + c.colorAccent = "#ff0000" + return c + }, + func(c *badge.ThemeColors) (string, string) { return c.Accent, "#ff0000" }, + }, + { + "positive", + func() BadgeConfig { + c := baseConf() + c.colorPositive = "#00ff00" + return c + }, + func(c *badge.ThemeColors) (string, string) { return c.Positive, "#00ff00" }, + }, + { + "negative", + func() BadgeConfig { + c := baseConf() + c.colorNegative = "#ff00ff" + return c + }, + func(c *badge.ThemeColors) (string, string) { return c.Negative, "#ff00ff" }, + }, + { + "star", + func() BadgeConfig { + c := baseConf() + c.colorStar = "#ffff00" + return c + }, + func(c *badge.ThemeColors) (string, string) { return c.Star, "#ffff00" }, + }, + } + + for _, tt := range colorFields { + t.Run(tt.name, func(t *testing.T) { + opts, err := createBadgeOptions(tt.makeConf()) + if err != nil { + t.Fatalf("createBadgeOptions failed: %v", err) + } + if opts.CustomColors == nil { + t.Fatal("CustomColors is nil, want non-nil") + } + got, want := tt.check(opts.CustomColors) + if got != want { + t.Errorf("got %q, want %q", got, want) + } + }) + } +} + +// baseConf returns a minimal valid BadgeConfig with no color overrides. +func baseConf() BadgeConfig { + return BadgeConfig{ + style: string(badge.DefaultBadgeStyle), + variant: string(badge.DefaultBadgeVariant), + theme: string(badge.DefaultBadgeTheme), + sort: string(badge.DefaultSortBy), + limit: badge.DefaultPRsLimit, + } +} + +func TestValidateHexColor(t *testing.T) { + tests := []struct { + value string + wantErr bool + }{ + // Valid + {"#abc", false}, + {"#ABC", false}, + {"#1a2b3c", false}, + {"#1A2B3C", false}, + {"#00000000", false}, // 8-digit with alpha + // Invalid + {"", true}, // empty (callers skip empty, but function itself rejects) + {"abc", true}, // missing # + {"#gg0000", true}, // invalid hex digits + {"#12345", true}, // 5 digits + {"#1234567", true}, // 7 digits + {"#123456789", true}, // 9 digits + {"red", true}, // named color + {"rgb(0,0,0)", true}, // CSS function + } + + for _, tt := range tests { + t.Run(tt.value, func(t *testing.T) { + err := validateHexColor("badge-color-accent", tt.value) + if (err != nil) != tt.wantErr { + t.Errorf("validateHexColor(%q) error = %v, wantErr %v", tt.value, err, tt.wantErr) + } + }) + } +} + +func TestValidateHexColorErrorMessage(t *testing.T) { + err := validateHexColor("badge-color-accent", "notacolor") + if err == nil { + t.Fatal("expected error, got nil") + } + msg := err.Error() + if !contains(msg, "--badge-color-accent") { + t.Errorf("error message should mention the flag name, got: %q", msg) + } + if !contains(msg, "notacolor") { + t.Errorf("error message should mention the invalid value, got: %q", msg) + } +} + +func TestCreateBadgeOptionsInvalidColor(t *testing.T) { + colorFlagFields := []struct { + flagName string + setConf func(*BadgeConfig) + }{ + {"badge-color-background", func(c *BadgeConfig) { c.colorBackground = "red" }}, + {"badge-color-background-alt", func(c *BadgeConfig) { c.colorBackgroundAlt = "blue" }}, + {"badge-color-text", func(c *BadgeConfig) { c.colorText = "white" }}, + {"badge-color-text-secondary", func(c *BadgeConfig) { c.colorTextSecondary = "gray" }}, + {"badge-color-border", func(c *BadgeConfig) { c.colorBorder = "black" }}, + {"badge-color-accent", func(c *BadgeConfig) { c.colorAccent = "invalid" }}, + {"badge-color-positive", func(c *BadgeConfig) { c.colorPositive = "green" }}, + {"badge-color-negative", func(c *BadgeConfig) { c.colorNegative = "rgb(255,0,0)" }}, + {"badge-color-star", func(c *BadgeConfig) { c.colorStar = "gold" }}, + } + + for _, tt := range colorFlagFields { + t.Run(tt.flagName, func(t *testing.T) { + conf := baseConf() + tt.setConf(&conf) + + _, err := createBadgeOptions(conf) + if err == nil { + t.Fatalf("expected error for invalid color on --%s, got nil", tt.flagName) + } + if !contains(err.Error(), "--"+tt.flagName) { + t.Errorf("error should mention the flag name --%s, got: %q", tt.flagName, err.Error()) + } + }) + } +} + +func TestCreateBadgeOptionsValidColors(t *testing.T) { + validColors := []string{"#fff", "#FFF", "#1a2b3c", "#1A2B3C", "#00000000"} + + for _, color := range validColors { + t.Run(color, func(t *testing.T) { + conf := baseConf() + conf.colorAccent = color + + _, err := createBadgeOptions(conf) + if err != nil { + t.Errorf("unexpected error for valid color %q: %v", color, err) + } + }) + } +} + +func contains(s, sub string) bool { + return len(s) >= len(sub) && (s == sub || len(sub) == 0 || + func() bool { + for i := 0; i <= len(s)-len(sub); i++ { + if s[i:i+len(sub)] == sub { + return true + } + } + return false + }()) +} diff --git a/docs/TECHNICAL.md b/docs/TECHNICAL.md index 035397f..6cf9446 100644 --- a/docs/TECHNICAL.md +++ b/docs/TECHNICAL.md @@ -183,14 +183,25 @@ These flags control badge generation and styling. Used by the main command (with | Flag | Type | Default | Description | |-------|-----------|-------------|-------------| | --badge | boolean | false | Generate SVG Badge (main command only) | +| --from-file | string | "" | Path to stats JSON file to generate badge from | +| --data | string | "" | Stats as JSON string | | --badge-style | string | summary | Badge style: `summary`, `compact`, `detailed` | | --badge-variant | string | default | Badge variant: `default`, `text-based` | | --badge-theme | string | dark | Color theme: `dark`, `light`, `nord`, `dracula`, `gruvbox-light`, `gruvbox-dark`, etc... | | --badge-output | string | ./badge.svg | Output file path for generated badge | | --badge-sort | string | prs | Sort contributions by: `prs`, `stars`, `commits` | | --badge-limit | int | 5 | Number of contributions to display in detailed badge | - - +| --badge-color-background | string | "" | Custom main background color (hex, e.g. `#1a1b26`) | +| --badge-color-background-alt | string | "" | Custom alt background color (hex) | +| --badge-color-text | string | "" | Custom primary text color (hex) | +| --badge-color-text-secondary | string | "" | Custom secondary/label text color (hex) | +| --badge-color-border | string | "" | Custom border/divider color (hex) | +| --badge-color-accent | string | "" | Custom accent/brand color (hex) | +| --badge-color-positive | string | "" | Custom positive/additions color (hex) | +| --badge-color-negative | string | "" | Custom negative/deletions color (hex) | +| --badge-color-star | string | "" | Custom star count color (hex) | + +> Custom color flags override individual colors from the selected theme. Unset flags retain the theme's color. ### Badge Generation diff --git a/docs/badges/default-detailed-catppuccin-frappe.svg b/docs/badges/default-detailed-catppuccin-frappe.svg index ab0cc0c..253a26a 100644 --- a/docs/badges/default-detailed-catppuccin-frappe.svg +++ b/docs/badges/default-detailed-catppuccin-frappe.svg @@ -155,8 +155,8 @@ font-family="Inter, system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" - letter-spacing="0em">android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number android-public class="repo-name" + letter-spacing="0em">android-public Tomato class="repo-name" + letter-spacing="0em">Tomato JetpackComposeTracker class="repo-name" + letter-spacing="0em">JetpackComposeTracker nav3-recipes class="repo-name" + letter-spacing="0em">nav3-recipes cahier class="repo-name" + letter-spacing="0em">cahier Compose-Rolling-Number class="repo-name" + letter-spacing="0em">Compose-Rolling-Number - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-catppuccin-latte.svg b/docs/badges/text-based-detailed-catppuccin-latte.svg index 5d3d2f5..4ca43af 100644 --- a/docs/badges/text-based-detailed-catppuccin-latte.svg +++ b/docs/badges/text-based-detailed-catppuccin-latte.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-catppuccin-macchiato.svg b/docs/badges/text-based-detailed-catppuccin-macchiato.svg index 9cce519..5ec5ca2 100644 --- a/docs/badges/text-based-detailed-catppuccin-macchiato.svg +++ b/docs/badges/text-based-detailed-catppuccin-macchiato.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-catppuccin-mocha.svg b/docs/badges/text-based-detailed-catppuccin-mocha.svg index 2e7a202..f3df399 100644 --- a/docs/badges/text-based-detailed-catppuccin-mocha.svg +++ b/docs/badges/text-based-detailed-catppuccin-mocha.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-dark.svg b/docs/badges/text-based-detailed-dark.svg index 557221f..e91f205 100644 --- a/docs/badges/text-based-detailed-dark.svg +++ b/docs/badges/text-based-detailed-dark.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-dracula.svg b/docs/badges/text-based-detailed-dracula.svg index 2b5a6d6..170a446 100644 --- a/docs/badges/text-based-detailed-dracula.svg +++ b/docs/badges/text-based-detailed-dracula.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-gruvbox-dark.svg b/docs/badges/text-based-detailed-gruvbox-dark.svg index 7c46032..a37db02 100644 --- a/docs/badges/text-based-detailed-gruvbox-dark.svg +++ b/docs/badges/text-based-detailed-gruvbox-dark.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-gruvbox-light.svg b/docs/badges/text-based-detailed-gruvbox-light.svg index 83967fa..9b2a6b1 100644 --- a/docs/badges/text-based-detailed-gruvbox-light.svg +++ b/docs/badges/text-based-detailed-gruvbox-light.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-light.svg b/docs/badges/text-based-detailed-light.svg index 2afbc48..60834df 100644 --- a/docs/badges/text-based-detailed-light.svg +++ b/docs/badges/text-based-detailed-light.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-monokai.svg b/docs/badges/text-based-detailed-monokai.svg index 7a92663..4238294 100644 --- a/docs/badges/text-based-detailed-monokai.svg +++ b/docs/badges/text-based-detailed-monokai.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-nord.svg b/docs/badges/text-based-detailed-nord.svg index ba6c515..8d719ad 100644 --- a/docs/badges/text-based-detailed-nord.svg +++ b/docs/badges/text-based-detailed-nord.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-one-dark-vivid.svg b/docs/badges/text-based-detailed-one-dark-vivid.svg index 16b53b1..fbb7137 100644 --- a/docs/badges/text-based-detailed-one-dark-vivid.svg +++ b/docs/badges/text-based-detailed-one-dark-vivid.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-one-dark.svg b/docs/badges/text-based-detailed-one-dark.svg index 5d21dbf..0222874 100644 --- a/docs/badges/text-based-detailed-one-dark.svg +++ b/docs/badges/text-based-detailed-one-dark.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-solarized-dark.svg b/docs/badges/text-based-detailed-solarized-dark.svg index f580c66..fa08a59 100644 --- a/docs/badges/text-based-detailed-solarized-dark.svg +++ b/docs/badges/text-based-detailed-solarized-dark.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-solarized-light.svg b/docs/badges/text-based-detailed-solarized-light.svg index ba51075..901240d 100644 --- a/docs/badges/text-based-detailed-solarized-light.svg +++ b/docs/badges/text-based-detailed-solarized-light.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-tokyo-night-light.svg b/docs/badges/text-based-detailed-tokyo-night-light.svg index c05fecd..48c3a51 100644 --- a/docs/badges/text-based-detailed-tokyo-night-light.svg +++ b/docs/badges/text-based-detailed-tokyo-night-light.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-tokyo-night-storm.svg b/docs/badges/text-based-detailed-tokyo-night-storm.svg index 48a924a..9460796 100644 --- a/docs/badges/text-based-detailed-tokyo-night-storm.svg +++ b/docs/badges/text-based-detailed-tokyo-night-storm.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/badges/text-based-detailed-tokyo-night.svg b/docs/badges/text-based-detailed-tokyo-night.svg index 3e4cac9..39a1935 100644 --- a/docs/badges/text-based-detailed-tokyo-night.svg +++ b/docs/badges/text-based-detailed-tokyo-night.svg @@ -123,7 +123,7 @@ - android-public + @ibad-al-rahman/android-public β˜… 15 @@ -134,7 +134,7 @@ - Tomato + @nsh07/Tomato β˜… 15 @@ -145,7 +145,7 @@ - JetpackComposeTracker + @qamarelsafadi/JetpackComposeTracker β˜… 15 @@ -156,7 +156,7 @@ - nav3-recipes + @android/nav3-recipes β˜… 15 @@ -167,7 +167,7 @@ - cahier + @android/cahier β˜… 15 @@ -178,7 +178,7 @@ - Compose-Rolling-Number + @esatgozcu/Compose-Rolling-Number β˜… 15 diff --git a/docs/release-notes/v0.3.3.md b/docs/release-notes/v0.3.3.md index 76c3e85..dfb8f52 100644 --- a/docs/release-notes/v0.3.3.md +++ b/docs/release-notes/v0.3.3.md @@ -1,6 +1,6 @@ # Release Notes: v0.3.3 -Release Date: Feb 13, 2025 +Release Date: Feb 13, 2026 Codename: Update template design + support 5 new color schemes ## ✨ Features diff --git a/docs/release-notes/v0.3.4.md b/docs/release-notes/v0.3.4.md new file mode 100644 index 0000000..cc2a510 --- /dev/null +++ b/docs/release-notes/v0.3.4.md @@ -0,0 +1,20 @@ +# Release Notes: v0.3.4 + +Release Date: Feb 13, 2026 +Codename: Fix text overflow + + +## ✨ Features + +- NEW: Added custom badge theme colors cli-flags + + +## ♻️ Improvements + +- Add **ownerName** to text-based variant, detailed style badge + + +## πŸ› Bug Fixes + +- **Detailed Badge**: Prevent text-overflow by truncating repoName + ownerName + diff --git a/pkg/ossstats/badge/badge.go b/pkg/ossstats/badge/badge.go index fd2128e..bdc2985 100644 --- a/pkg/ossstats/badge/badge.go +++ b/pkg/ossstats/badge/badge.go @@ -49,6 +49,36 @@ func RenderSVG(stats *ossstats.Stats, opts BadgeOptions) (string, error) { // Get theme colors colors := GetThemeColors(opts.Theme) + if opts.CustomColors != nil { + c := opts.CustomColors + if c.Background != "" { + colors.Background = c.Background + } + if c.BackgroundAlt != "" { + colors.BackgroundAlt = c.BackgroundAlt + } + if c.Text != "" { + colors.Text = c.Text + } + if c.TextSecondary != "" { + colors.TextSecondary = c.TextSecondary + } + if c.Border != "" { + colors.Border = c.Border + } + if c.Accent != "" { + colors.Accent = c.Accent + } + if c.Positive != "" { + colors.Positive = c.Positive + } + if c.Negative != "" { + colors.Negative = c.Negative + } + if c.Star != "" { + colors.Star = c.Star + } + } // Prepare base template data data := templateData{ @@ -74,11 +104,12 @@ func RenderSVG(stats *ossstats.Stats, opts BadgeOptions) (string, error) { // Parse and execute template with custom functions tmpl, err := template.New("badge").Funcs(template.FuncMap{ - "add": func(a, b int) int { return a + b }, - "sub": func(a, b int) int { return a - b }, - "mul": func(a, b int) int { return a * b }, - "mod": func(a, b int) int { return a % b }, - "div": func(a, b int) int { return a / b }, + "add": func(a, b int) int { return a + b }, + "sub": func(a, b int) int { return a - b }, + "mul": func(a, b int) int { return a * b }, + "mod": func(a, b int) int { return a % b }, + "div": func(a, b int) int { return a / b }, + "truncate": truncate, }).Parse(tmplStr) if err != nil { return "", fmt.Errorf("failed to parse template: %w", err) @@ -175,3 +206,11 @@ func getTemplateStr( err := fmt.Errorf("unsupported badge variant: %s, and style: %s combinations", variant, style) return "", err } + +func truncate(maxLen int, s string) string { + runes := []rune(s) + if len(runes) <= maxLen { + return s + } + return string(runes[:maxLen-1]) + "…" +} diff --git a/pkg/ossstats/badge/badgeTemplates/defaults.go b/pkg/ossstats/badge/badgeTemplates/defaults.go index 4b8d8b0..c96e75d 100644 --- a/pkg/ossstats/badge/badgeTemplates/defaults.go +++ b/pkg/ossstats/badge/badgeTemplates/defaults.go @@ -262,7 +262,7 @@ const DefaultDetailed = ` font-size="16" font-weight="bold" class="repo-name" - letter-spacing="0em">{{$r.RepoName}} + letter-spacing="0em">{{truncate 26 $r.RepoName}} @{{$r.Owner}} + letter-spacing="0em">@{{truncate 28 $r.Owner}} - {{$r.RepoName}} + @{{truncate 52 (printf "%s/%s" $r.Owner $r.RepoName)}} β˜… {{$r.Stars}} diff --git a/pkg/ossstats/badge/badge_test.go b/pkg/ossstats/badge/badge_test.go index 489eb1d..5854cee 100644 --- a/pkg/ossstats/badge/badge_test.go +++ b/pkg/ossstats/badge/badge_test.go @@ -597,6 +597,204 @@ func TestGetThemeColors(t *testing.T) { } } +func TestTruncate(t *testing.T) { + tests := []struct { + name string + maxLen int + input string + want string + }{ + { + name: "short string unchanged", + maxLen: 10, + input: "hello", + want: "hello", + }, + { + name: "exact length unchanged", + maxLen: 5, + input: "hello", + want: "hello", + }, + { + name: "over limit gets ellipsis", + maxLen: 5, + input: "hello world", + want: "hell…", + }, + { + name: "empty string unchanged", + maxLen: 10, + input: "", + want: "", + }, + { + name: "multibyte runes truncated correctly", + maxLen: 5, + input: "ζ—₯本θͺžγƒ†γ‚Ήγƒˆι•·γ„名前", + want: "ζ—₯本θͺžγƒ†β€¦", + }, + { + name: "limit 1 yields only ellipsis", + maxLen: 1, + input: "ab", + want: "…", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := truncate(tt.maxLen, tt.input) + if got != tt.want { + t.Errorf("truncate(%d, %q) = %q, want %q", tt.maxLen, tt.input, got, tt.want) + } + }) + } + +} + +func TestRenderSVG_DetailedTextTruncation(t *testing.T) { + tests := []struct { + name string + repoName string + owner string + wantRepoName string + wantOwner string + badgeVariant BadgeVariant + }{ + { + name: "short names pass through unchanged", + repoName: "short-repo", + owner: "shortowner", + wantRepoName: "short-repo", + wantOwner: "@shortowner", + badgeVariant: VariantDefault, + }, + { + name: "long repo name is truncated", + repoName: "this-repo-name-is-way-too-long-to-fit", + owner: "owner", + wantRepoName: "this-repo-name-is-way-too…", + wantOwner: "@owner", + badgeVariant: VariantDefault, + }, + { + name: "long owner name is truncated", + repoName: "repo", + owner: "this-owner-name-is-way-too-long-to-fit", + wantRepoName: "repo", + wantOwner: "@this-owner-name-is-way-too-…", + badgeVariant: VariantDefault, + }, + { + name: "both long names are truncated", + repoName: "a-very-long-repository-name-here", + owner: "a-very-long-organization-name-here", + wantRepoName: "a-very-long-repository-na…", + wantOwner: "@a-very-long-organization-na…", + badgeVariant: VariantDefault, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stats := &ossstats.Stats{ + Username: "testuser", + Summary: ossstats.Summary{ + TotalProjects: 1, + TotalPRsMerged: 1, + }, + Contributions: []ossstats.Contribution{ + { + RepoName: tt.repoName, + Owner: tt.owner, + Stars: 100, + PRsMerged: 1, + }, + }, + } + + opts := BadgeOptions{ + Style: StyleDetailed, + Variant: tt.badgeVariant, + Theme: ThemeGithubDark, + Limit: 1, + } + + svg, err := RenderSVG(stats, opts) + if err != nil { + t.Fatalf("RenderSVG() unexpected error: %v", err) + } + + if !strings.Contains(svg, tt.wantRepoName) { + t.Errorf("SVG missing expected repo name %q", tt.wantRepoName) + } + if !strings.Contains(svg, tt.wantOwner) { + t.Errorf("SVG missing expected owner %q", tt.wantOwner) + } + }) + } +} + +func TestRenderSVG_TextBasedDetailedTextTruncation(t *testing.T) { + tests := []struct { + name string + repoName string + owner string + wantRepoName string + wantOwner string + badgeVariant BadgeVariant + }{ + { + name: "both long names are truncated", + repoName: "a-veryyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy-very-very-very-long-repository-name-here", + owner: "testowner", + wantRepoName: "@testowner/a-veryyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy-ver…", + wantOwner: "@testowner", + badgeVariant: VariantTextBased, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stats := &ossstats.Stats{ + Username: "testuser", + Summary: ossstats.Summary{ + TotalProjects: 1, + TotalPRsMerged: 1, + }, + Contributions: []ossstats.Contribution{ + { + RepoName: tt.repoName, + Owner: tt.owner, + Stars: 100, + PRsMerged: 1, + }, + }, + } + + opts := BadgeOptions{ + Style: StyleDetailed, + Variant: tt.badgeVariant, + Theme: ThemeGithubDark, + Limit: 1, + } + + svg, err := RenderSVG(stats, opts) + if err != nil { + t.Fatalf("RenderSVG() unexpected error: %v", err) + } + + if !strings.Contains(svg, tt.wantRepoName) { + t.Errorf("SVG missing expected repo name %q", tt.wantRepoName) + } + if !strings.Contains(svg, tt.wantOwner) { + t.Errorf("SVG missing expected owner %q", tt.wantOwner) + } + }) + } +} + func TestRenderSVG_CompactBadgeContent(t *testing.T) { stats := &ossstats.Stats{ Username: "testuser", diff --git a/pkg/ossstats/badge/types.go b/pkg/ossstats/badge/types.go index 70b7284..c351eb9 100644 --- a/pkg/ossstats/badge/types.go +++ b/pkg/ossstats/badge/types.go @@ -2,9 +2,10 @@ package badge // BadgeOptions contains all configuration for badge generation type BadgeOptions struct { - Style BadgeStyle - Variant BadgeVariant - Theme BadgeTheme - SortBy SortBy // For detailed badge - how to sort contributions (default: prs) - Limit int // For detailed badge - max contributions to show (default: 5) + Style BadgeStyle + Variant BadgeVariant + Theme BadgeTheme + SortBy SortBy // For detailed badge - how to sort contributions (default: prs) + Limit int // For detailed badge - max contributions to show (default: 5) + CustomColors *ThemeColors // Optional per-color overrides (applied on top of Theme) }