summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-08 00:19:16 +0300
committerPaul Buetow <paul@buetow.org>2025-07-08 00:19:16 +0300
commitdef6195bfc4085fe805e966d9e1c4d40592747a1 (patch)
tree70a40bacefc0229311a2f5464516af08d570612e
parentcdd94f0cbc4c57e037f6e7143cf65d2e87d95eca (diff)
feat: show language usage percentages in showcase
- Languages are now counted by lines of code - Displayed with percentages ordered by usage (highest first) - Format: 'Go (88.8%), Markdown (5.5%), ...' - Only shows languages with at least 0.1% usage - More accurate representation of project composition 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
-rw-r--r--internal/showcase/language_detector.go235
-rw-r--r--internal/showcase/metadata.go119
-rw-r--r--internal/showcase/showcase.go15
3 files changed, 256 insertions, 113 deletions
diff --git a/internal/showcase/language_detector.go b/internal/showcase/language_detector.go
new file mode 100644
index 0000000..ac7bc75
--- /dev/null
+++ b/internal/showcase/language_detector.go
@@ -0,0 +1,235 @@
+package showcase
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+)
+
+// detectLanguages detects programming languages used in the repository with line counts
+func detectLanguages(repoPath string) ([]LanguageStats, error) {
+ languageLines := make(map[string]int)
+
+ // Define common language extensions
+ langExtensions := map[string]string{
+ ".go": "Go",
+ ".py": "Python",
+ ".js": "JavaScript",
+ ".ts": "TypeScript",
+ ".java": "Java",
+ ".c": "C",
+ ".cpp": "C++",
+ ".cc": "C++",
+ ".cxx": "C++",
+ ".h": "C/C++",
+ ".hpp": "C++",
+ ".hxx": "C++",
+ ".cs": "C#",
+ ".rb": "Ruby",
+ ".php": "PHP",
+ ".swift": "Swift",
+ ".kt": "Kotlin",
+ ".rs": "Rust",
+ ".scala": "Scala",
+ ".r": "R",
+ ".m": "Objective-C",
+ ".mm": "Objective-C++",
+ ".sh": "Shell",
+ ".bash": "Shell",
+ ".zsh": "Shell",
+ ".fish": "Shell",
+ ".pl": "Perl",
+ ".lua": "Lua",
+ ".vim": "Vim Script",
+ ".el": "Emacs Lisp",
+ ".clj": "Clojure",
+ ".hs": "Haskell",
+ ".ml": "OCaml",
+ ".ex": "Elixir",
+ ".exs": "Elixir",
+ ".dart": "Dart",
+ ".jl": "Julia",
+ ".nim": "Nim",
+ ".v": "V",
+ ".zig": "Zig",
+ ".html": "HTML",
+ ".htm": "HTML",
+ ".css": "CSS",
+ ".scss": "SCSS",
+ ".sass": "Sass",
+ ".less": "Less",
+ ".xml": "XML",
+ ".json": "JSON",
+ ".yaml": "YAML",
+ ".yml": "YAML",
+ ".toml": "TOML",
+ ".ini": "INI",
+ ".cfg": "Config",
+ ".conf": "Config",
+ ".sql": "SQL",
+ ".md": "Markdown",
+ ".rst": "reStructuredText",
+ ".tex": "LaTeX",
+ }
+
+ // Special files that indicate specific languages
+ specialFiles := map[string]string{
+ "makefile": "Make",
+ "gnumakefile": "Make",
+ "dockerfile": "Docker",
+ "dockerfile.*": "Docker",
+ "cmakelists.txt": "CMake",
+ "rakefile": "Ruby",
+ "gemfile": "Ruby",
+ "package.json": "JavaScript",
+ "cargo.toml": "Rust",
+ "go.mod": "Go",
+ "go.sum": "Go",
+ "pom.xml": "Java",
+ "build.gradle": "Gradle",
+ "build.gradle.kts": "Kotlin",
+ "requirements.txt": "Python",
+ "setup.py": "Python",
+ "pyproject.toml": "Python",
+ "composer.json": "PHP",
+ "*.dockerfile": "Docker",
+ "containerfile": "Docker",
+ "jenkinsfile": "Groovy",
+ "vagrantfile": "Ruby",
+ }
+
+ // Count lines for each language
+ err := filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return nil // Skip errors
+ }
+
+ // Skip directories
+ if info.IsDir() {
+ name := info.Name()
+ // Skip hidden directories and common non-code directories
+ if strings.HasPrefix(name, ".") && name != "." ||
+ name == "node_modules" ||
+ name == "vendor" ||
+ name == "target" ||
+ name == "dist" ||
+ name == "build" ||
+ name == "out" ||
+ name == "__pycache__" ||
+ name == "coverage" {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+
+ // Skip binary and large files
+ if info.Size() > 10*1024*1024 { // Skip files larger than 10MB
+ return nil
+ }
+
+ // Get the filename and extension
+ basename := strings.ToLower(filepath.Base(path))
+ ext := strings.ToLower(filepath.Ext(path))
+
+ // Determine the language
+ var language string
+
+ // Check special files first
+ if lang, ok := specialFiles[basename]; ok {
+ language = lang
+ } else {
+ // Check by extension
+ if lang, ok := langExtensions[ext]; ok {
+ language = lang
+ }
+ }
+
+ // If we identified a language, count its lines
+ if language != "" {
+ lines, err := countFileLines(path)
+ if err == nil {
+ languageLines[language] += lines
+ }
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ // Calculate total lines
+ totalLines := 0
+ for _, lines := range languageLines {
+ totalLines += lines
+ }
+
+ // Convert to LanguageStats with percentages
+ var stats []LanguageStats
+ for lang, lines := range languageLines {
+ percentage := 0.0
+ if totalLines > 0 {
+ percentage = float64(lines) * 100.0 / float64(totalLines)
+ }
+ stats = append(stats, LanguageStats{
+ Name: lang,
+ Lines: lines,
+ Percentage: percentage,
+ })
+ }
+
+ // Sort by percentage (descending)
+ sort.Slice(stats, func(i, j int) bool {
+ return stats[i].Percentage > stats[j].Percentage
+ })
+
+ return stats, nil
+}
+
+// countFileLines counts the number of lines in a file
+func countFileLines(path string) (int, error) {
+ file, err := os.Open(path)
+ if err != nil {
+ return 0, err
+ }
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ lines := 0
+ for scanner.Scan() {
+ lines++
+ }
+
+ if err := scanner.Err(); err != nil {
+ return 0, err
+ }
+
+ return lines, nil
+}
+
+// FormatLanguagesWithPercentages formats languages with their percentages
+func FormatLanguagesWithPercentages(languages []LanguageStats) string {
+ if len(languages) == 0 {
+ return ""
+ }
+
+ var parts []string
+ for _, lang := range languages {
+ if lang.Percentage >= 0.1 { // Only show languages with at least 0.1%
+ parts = append(parts, fmt.Sprintf("%s (%.1f%%)", lang.Name, lang.Percentage))
+ }
+ }
+
+ // If all languages are below 0.1%, just show the names
+ if len(parts) == 0 {
+ for _, lang := range languages {
+ parts = append(parts, lang.Name)
+ }
+ }
+
+ return strings.Join(parts, ", ")
+} \ No newline at end of file
diff --git a/internal/showcase/metadata.go b/internal/showcase/metadata.go
index a2cfa47..713af90 100644
--- a/internal/showcase/metadata.go
+++ b/internal/showcase/metadata.go
@@ -10,9 +10,16 @@ import (
"time"
)
+// LanguageStats holds statistics for a programming language
+type LanguageStats struct {
+ Name string
+ Lines int
+ Percentage float64
+}
+
// RepoMetadata holds metadata about a repository
type RepoMetadata struct {
- Languages []string
+ Languages []LanguageStats // Languages with usage statistics
CommitCount int
LinesOfCode int
FirstCommitDate string
@@ -73,116 +80,6 @@ func extractRepoMetadata(repoPath string) (*RepoMetadata, error) {
return metadata, nil
}
-// detectLanguages detects programming languages used in the repository
-func detectLanguages(repoPath string) ([]string, error) {
- languageMap := make(map[string]bool)
-
- // Define common language extensions
- langExtensions := map[string]string{
- ".go": "Go",
- ".py": "Python",
- ".js": "JavaScript",
- ".ts": "TypeScript",
- ".java": "Java",
- ".c": "C",
- ".cpp": "C++",
- ".cc": "C++",
- ".h": "C/C++",
- ".hpp": "C++",
- ".cs": "C#",
- ".rb": "Ruby",
- ".php": "PHP",
- ".swift": "Swift",
- ".kt": "Kotlin",
- ".rs": "Rust",
- ".scala": "Scala",
- ".r": "R",
- ".m": "Objective-C",
- ".mm": "Objective-C++",
- ".sh": "Shell",
- ".bash": "Bash",
- ".zsh": "Zsh",
- ".pl": "Perl",
- ".lua": "Lua",
- ".vim": "Vim Script",
- ".el": "Emacs Lisp",
- ".clj": "Clojure",
- ".hs": "Haskell",
- ".ml": "OCaml",
- ".ex": "Elixir",
- ".exs": "Elixir",
- ".dart": "Dart",
- ".jl": "Julia",
- ".nim": "Nim",
- ".v": "V",
- ".zig": "Zig",
- }
-
- // Walk through the repository
- err := filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return nil // Skip errors
- }
-
- // Skip hidden directories and common non-code directories
- if info.IsDir() {
- name := info.Name()
- if strings.HasPrefix(name, ".") || name == "node_modules" || name == "vendor" || name == "target" || name == "dist" || name == "build" {
- return filepath.SkipDir
- }
- return nil
- }
-
- // Check file extension
- ext := strings.ToLower(filepath.Ext(path))
- if lang, ok := langExtensions[ext]; ok {
- languageMap[lang] = true
- }
-
- // Check for special files
- basename := filepath.Base(path)
- switch strings.ToLower(basename) {
- case "makefile", "gnumakefile":
- languageMap["Make"] = true
- case "dockerfile":
- languageMap["Docker"] = true
- case "cmakelists.txt":
- languageMap["CMake"] = true
- case "rakefile":
- languageMap["Ruby"] = true
- case "gemfile":
- languageMap["Ruby"] = true
- case "package.json":
- languageMap["JavaScript/Node.js"] = true
- case "cargo.toml":
- languageMap["Rust"] = true
- case "go.mod":
- languageMap["Go"] = true
- case "pom.xml":
- languageMap["Java/Maven"] = true
- case "build.gradle", "build.gradle.kts":
- languageMap["Java/Gradle"] = true
- case "requirements.txt", "setup.py", "pyproject.toml":
- languageMap["Python"] = true
- case "composer.json":
- languageMap["PHP"] = true
- }
-
- return nil
- })
-
- if err != nil {
- return nil, err
- }
-
- // Convert map to slice
- var languages []string
- for lang := range languageMap {
- languages = append(languages, lang)
- }
-
- return languages, nil
-}
// getCommitCount returns the total number of commits
func getCommitCount(repoPath string) (int, error) {
diff --git a/internal/showcase/showcase.go b/internal/showcase/showcase.go
index 6a9cbae..386a674 100644
--- a/internal/showcase/showcase.go
+++ b/internal/showcase/showcase.go
@@ -29,6 +29,17 @@ type ProjectSummary struct {
Images []string // Relative paths to images in showcase directory
}
+// LegacyRepoMetadata for backwards compatibility with old cache files
+type LegacyRepoMetadata struct {
+ Languages []string
+ CommitCount int
+ LinesOfCode int
+ FirstCommitDate string
+ LastCommitDate string
+ License string
+ AvgCommitAge float64
+}
+
// New creates a new showcase generator
func New(cfg *config.Config, workDir string) *Generator {
return &Generator{
@@ -82,7 +93,7 @@ func (g *Generator) GenerateShowcase(repoFilter []string, forceRegenerate bool)
fmt.Printf("\n--- Generated summary for %s ---\n", repo)
fmt.Println(summary.Summary)
if summary.Metadata != nil {
- fmt.Printf("Languages: %s\n", strings.Join(summary.Metadata.Languages, ", "))
+ fmt.Printf("Languages: %s\n", FormatLanguagesWithPercentages(summary.Metadata.Languages))
fmt.Printf("Commits: %d\n", summary.Metadata.CommitCount)
fmt.Printf("Lines of Code: %d\n", summary.Metadata.LinesOfCode)
fmt.Printf("First Commit: %s\n", summary.Metadata.FirstCommitDate)
@@ -293,7 +304,7 @@ func (g *Generator) formatGemtext(summaries []ProjectSummary) string {
// Add metadata if available
if summary.Metadata != nil {
if len(summary.Metadata.Languages) > 0 {
- builder.WriteString(fmt.Sprintf("* Languages: %s\n", strings.Join(summary.Metadata.Languages, ", ")))
+ builder.WriteString(fmt.Sprintf("* Languages: %s\n", FormatLanguagesWithPercentages(summary.Metadata.Languages)))
}
builder.WriteString(fmt.Sprintf("* Commits: %d\n", summary.Metadata.CommitCount))
builder.WriteString(fmt.Sprintf("* Lines of Code: %d\n", summary.Metadata.LinesOfCode))