summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-08 23:20:48 +0300
committerPaul Buetow <paul@buetow.org>2025-07-08 23:20:48 +0300
commit4abc942bd49c6633041d7822bc75d5b64e11899b (patch)
tree72904d33547e786a4909d400cdb2a836d9da3d3a
parent81b6357a96e684d7588e499c8a7f3ab892c8c02a (diff)
feat: improve showcase code snippets and add Unicode icons
- Refactor code snippet extraction to show complete functions (5-50 lines) - Add findSmallestCompleteFunction to prioritize smaller, complete code units - Move code samples to the end of project descriptions (after links) - Remove '### Code Sample' header for cleaner output - Add AI detection for CLAUDE.md, GEMINI.md, and 'agentic coding' mentions - Add AI-Assisted indicator when AI usage is detected - Add Unicode icons to all statistics for better visual presentation: * 📦 Projects, 📊 Commits, 📈 Lines of Code, 📄 Documentation * 💻 Languages, 📚 Documentation types, 📅 Development Period * 🔥 Recent Activity, ⚖️ License, 🤖 AI-Assisted - Add HCL/Terraform language support (.tf, .tfvars, .hcl files) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
-rw-r--r--internal/showcase/code_extractor.go395
-rw-r--r--internal/showcase/language_detector.go3
-rw-r--r--internal/showcase/showcase.go111
3 files changed, 482 insertions, 27 deletions
diff --git a/internal/showcase/code_extractor.go b/internal/showcase/code_extractor.go
new file mode 100644
index 0000000..e8ba0d3
--- /dev/null
+++ b/internal/showcase/code_extractor.go
@@ -0,0 +1,395 @@
+package showcase
+
+import (
+ "bufio"
+ "fmt"
+ "math/rand"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
+
+// extractCodeSnippet extracts a random code snippet from the repository
+func extractCodeSnippet(repoPath string, languages []LanguageStats) (string, string, error) {
+ if len(languages) == 0 {
+ return "", "", fmt.Errorf("no programming languages found")
+ }
+
+ // Get the primary language (highest percentage)
+ primaryLang := languages[0].Name
+
+ // Define file extensions for each language
+ langExtensions := map[string][]string{
+ "Go": {".go"},
+ "Python": {".py"},
+ "JavaScript": {".js"},
+ "TypeScript": {".ts"},
+ "Java": {".java"},
+ "C": {".c", ".h"},
+ "C++": {".cpp", ".cc", ".cxx", ".hpp"},
+ "C/C++": {".h"},
+ "C#": {".cs"},
+ "Ruby": {".rb"},
+ "PHP": {".php"},
+ "Swift": {".swift"},
+ "Kotlin": {".kt"},
+ "Rust": {".rs"},
+ "Shell": {".sh", ".bash"},
+ "Perl": {".pl", ".pm"},
+ "Haskell": {".hs"},
+ "Lua": {".lua"},
+ "HTML": {".html", ".htm"},
+ "CSS": {".css"},
+ "SQL": {".sql"},
+ "Make": {"Makefile", "makefile", "GNUmakefile"},
+ "HCL": {".tf", ".tfvars", ".hcl"},
+ }
+
+ // Get file extensions for the primary language
+ extensions, ok := langExtensions[primaryLang]
+ if !ok {
+ // Try other languages if primary doesn't have extensions defined
+ for _, lang := range languages {
+ if exts, exists := langExtensions[lang.Name]; exists {
+ extensions = exts
+ primaryLang = lang.Name
+ break
+ }
+ }
+ if len(extensions) == 0 {
+ return "", "", fmt.Errorf("no known file extensions for languages")
+ }
+ }
+
+ // Find all files matching the extensions
+ var codeFiles []string
+ err := filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return nil
+ }
+
+ // 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 == "__pycache__" {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+
+ // Skip files that are too large
+ if info.Size() > 1*1024*1024 { // 1MB
+ return nil
+ }
+
+ // Check if file matches extensions
+ basename := filepath.Base(path)
+ ext := filepath.Ext(path)
+
+ for _, validExt := range extensions {
+ if validExt == basename || (strings.HasPrefix(validExt, ".") && ext == validExt) {
+ // Skip test files and generated files
+ if !strings.Contains(basename, "_test") &&
+ !strings.Contains(basename, ".test.") &&
+ !strings.Contains(basename, ".min.") &&
+ !strings.Contains(path, "/test/") &&
+ !strings.Contains(path, "/tests/") {
+ codeFiles = append(codeFiles, path)
+ }
+ break
+ }
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return "", "", err
+ }
+
+ if len(codeFiles) == 0 {
+ return "", "", fmt.Errorf("no code files found")
+ }
+
+ // Select a random file
+ selectedFile := codeFiles[rand.Intn(len(codeFiles))]
+
+ // Read the file and extract a snippet (~10 lines but complete functions)
+ snippet, err := extractSnippetFromFile(selectedFile, 10, 15)
+ if err != nil {
+ return "", "", err
+ }
+
+ // Get relative path for display
+ relPath, _ := filepath.Rel(repoPath, selectedFile)
+
+ return snippet, fmt.Sprintf("%s from `%s`", primaryLang, relPath), nil
+}
+
+// extractSnippetFromFile extracts a code snippet from a file
+func extractSnippetFromFile(filePath string, minLines, maxLines int) (string, error) {
+ file, err := os.Open(filePath)
+ if err != nil {
+ return "", err
+ }
+ defer file.Close()
+
+ // Read all lines
+ var lines []string
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ lines = append(lines, scanner.Text())
+ }
+
+ if err := scanner.Err(); err != nil {
+ return "", err
+ }
+
+ totalLines := len(lines)
+ if totalLines == 0 {
+ return "", fmt.Errorf("file is empty")
+ }
+
+ // Try to find the smallest complete function
+ bestFunction := findSmallestCompleteFunction(lines)
+ if bestFunction != "" {
+ return bestFunction, nil
+ }
+
+ // If no complete function found, try to find a complete function/method
+ functionStart, functionEnd := findCompleteFunctionOrMethod(lines, minLines, maxLines*2) // Allow larger functions
+ if functionStart >= 0 && functionEnd >= 0 {
+ return strings.Join(lines[functionStart:functionEnd+1], "\n"), nil
+ }
+
+ // Fallback to finding an interesting start with at least minLines
+ interestingStart := findInterestingStart(lines, minLines)
+ if interestingStart >= 0 {
+ endLine := interestingStart + minLines
+ if endLine > totalLines {
+ endLine = totalLines
+ }
+ return strings.Join(lines[interestingStart:endLine], "\n"), nil
+ }
+
+ // Last resort: return first minLines (skip imports if possible)
+ skipLines := 0
+ for i, line := range lines {
+ trimmed := strings.TrimSpace(line)
+ if trimmed != "" && !strings.HasPrefix(trimmed, "import") &&
+ !strings.HasPrefix(trimmed, "package") && !strings.HasPrefix(trimmed, "using") &&
+ !strings.HasPrefix(trimmed, "#include") && !strings.HasPrefix(trimmed, "from") {
+ skipLines = i
+ break
+ }
+ }
+
+ endLine := skipLines + minLines
+ if endLine > totalLines {
+ endLine = totalLines
+ }
+
+ return strings.Join(lines[skipLines:endLine], "\n"), nil
+}
+
+// findSmallestCompleteFunction finds the smallest complete function in the file
+func findSmallestCompleteFunction(lines []string) string {
+ type functionInfo struct {
+ start int
+ end int
+ size int
+ }
+
+ var functions []functionInfo
+
+ // Keywords that typically start functions/methods
+ functionKeywords := []string{
+ "func ", "function ", "def ", "public ", "private ", "protected ",
+ "static ", "async ", "procedure ", "sub ", "method ",
+ }
+
+ // Find all complete functions
+ for i := 0; i < len(lines); i++ {
+ line := strings.TrimSpace(lines[i])
+
+ // Check if this line starts a function
+ isFunction := false
+ for _, keyword := range functionKeywords {
+ if strings.Contains(line, keyword) && !strings.HasPrefix(line, "//") && !strings.HasPrefix(line, "#") {
+ isFunction = true
+ break
+ }
+ }
+
+ if !isFunction {
+ continue
+ }
+
+ // Try to find the end of this function
+ functionEnd := findFunctionEnd(lines, i)
+ if functionEnd > i {
+ size := functionEnd - i + 1
+ // Only consider functions between 5 and 50 lines
+ if size >= 5 && size <= 50 {
+ functions = append(functions, functionInfo{
+ start: i,
+ end: functionEnd,
+ size: size,
+ })
+ }
+ }
+ }
+
+ // Find the smallest function
+ if len(functions) > 0 {
+ smallest := functions[0]
+ for _, f := range functions[1:] {
+ if f.size < smallest.size {
+ smallest = f
+ }
+ }
+ return strings.Join(lines[smallest.start:smallest.end+1], "\n")
+ }
+
+ return ""
+}
+
+// findFunctionEnd finds the end of a function starting at the given line
+func findFunctionEnd(lines []string, start int) int {
+ if start >= len(lines) {
+ return -1
+ }
+
+ // For brace-based languages
+ braceCount := 0
+ inFunction := false
+
+ // For Python - track initial indentation
+ isPython := strings.Contains(lines[start], "def ") || strings.Contains(lines[start], "class ")
+ var initialIndent int
+ if isPython && start < len(lines)-1 {
+ // Get indentation of first line after def
+ for i := start + 1; i < len(lines); i++ {
+ if strings.TrimSpace(lines[i]) != "" {
+ initialIndent = len(lines[i]) - len(strings.TrimLeft(lines[i], " \t"))
+ break
+ }
+ }
+ }
+
+ for i := start; i < len(lines); i++ {
+ line := lines[i]
+ trimmed := strings.TrimSpace(line)
+
+ // Handle Python indentation
+ if isPython && i > start {
+ if trimmed == "" {
+ continue
+ }
+ currentIndent := len(line) - len(strings.TrimLeft(line, " \t"))
+ if currentIndent < initialIndent {
+ return i - 1
+ }
+ }
+
+ // Handle brace-based languages
+ for _, ch := range line {
+ if ch == '{' {
+ braceCount++
+ inFunction = true
+ } else if ch == '}' {
+ braceCount--
+ if braceCount == 0 && inFunction {
+ return i
+ }
+ }
+ }
+ }
+
+ // If we're in Python and reached the end, return the last line
+ if isPython {
+ return len(lines) - 1
+ }
+
+ return -1
+}
+
+// findCompleteFunctionOrMethod finds a complete function or method within size constraints
+func findCompleteFunctionOrMethod(lines []string, minLines, maxLines int) (int, int) {
+ // Keywords that typically start functions/methods
+ functionKeywords := []string{
+ "func ", "function ", "def ", "public ", "private ", "protected ",
+ "static ", "async ", "procedure ", "sub ", "method ",
+ }
+
+ // Try to find a function that fits within our size constraints
+ for i := 0; i < len(lines); i++ {
+ line := strings.TrimSpace(lines[i])
+
+ // Check if this line starts a function
+ isFunction := false
+ for _, keyword := range functionKeywords {
+ if strings.Contains(line, keyword) && !strings.HasPrefix(line, "//") && !strings.HasPrefix(line, "#") {
+ isFunction = true
+ break
+ }
+ }
+
+ if !isFunction {
+ continue
+ }
+
+ // Try to find the end of this function
+ functionEnd := findFunctionEnd(lines, i)
+ if functionEnd > i {
+ functionLength := functionEnd - i + 1
+ if functionLength >= minLines && functionLength <= maxLines {
+ return i, functionEnd
+ }
+ }
+ }
+
+ return -1, -1
+}
+
+// findInterestingStart tries to find a good starting point for the snippet
+func findInterestingStart(lines []string, snippetSize int) int {
+ // Look for function/class definitions
+ keywords := []string{
+ "func ", "function ", "def ", "class ", "public class",
+ "interface ", "struct ", "type ", "const ", "var ",
+ "procedure ", "sub ", "method ",
+ }
+
+ for i := 0; i < len(lines)-snippetSize; i++ {
+ line := strings.TrimSpace(lines[i])
+ // Skip empty lines and comments
+ if line == "" || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "#") ||
+ strings.HasPrefix(line, "/*") || strings.HasPrefix(line, "*") {
+ continue
+ }
+
+ // Check for interesting keywords
+ for _, keyword := range keywords {
+ if strings.Contains(line, keyword) {
+ // Found something interesting, start here
+ return i
+ }
+ }
+ }
+
+ // No interesting start found
+ return -1
+} \ No newline at end of file
diff --git a/internal/showcase/language_detector.go b/internal/showcase/language_detector.go
index d049aac..f99e8cb 100644
--- a/internal/showcase/language_detector.go
+++ b/internal/showcase/language_detector.go
@@ -72,6 +72,9 @@ func detectLanguages(repoPath string) (languages []LanguageStats, documentation
".cfg": "Config",
".conf": "Config",
".sql": "SQL",
+ ".tf": "HCL",
+ ".tfvars": "HCL",
+ ".hcl": "HCL",
}
// Define documentation/text extensions
diff --git a/internal/showcase/showcase.go b/internal/showcase/showcase.go
index aa3e959..f5fd297 100644
--- a/internal/showcase/showcase.go
+++ b/internal/showcase/showcase.go
@@ -21,12 +21,15 @@ type Generator struct {
// ProjectSummary holds the summary information for a project
type ProjectSummary struct {
- Name string
- Summary string
- CodebergURL string
- GitHubURL string
- Metadata *RepoMetadata
- Images []string // Relative paths to images in showcase directory
+ Name string
+ Summary string
+ CodebergURL string
+ GitHubURL string
+ Metadata *RepoMetadata
+ Images []string // Relative paths to images in showcase directory
+ CodeSnippet string // Code snippet to show when no images
+ CodeLanguage string // Language and file info for the snippet
+ AIAssisted bool // Whether AI was detected in the project
}
// LegacyRepoMetadata for backwards compatibility with old cache files
@@ -261,13 +264,31 @@ func (g *Generator) generateProjectSummary(repoName string, forceRegenerate bool
// Continue without images
}
+ // Extract code snippet if no images found
+ var codeSnippet, codeLanguage string
+ if len(images) == 0 && metadata != nil && len(metadata.Languages) > 0 {
+ snippet, lang, err := extractCodeSnippet(repoPath, metadata.Languages)
+ if err != nil {
+ fmt.Printf("Warning: Failed to extract code snippet: %v\n", err)
+ } else {
+ codeSnippet = snippet
+ codeLanguage = lang
+ }
+ }
+
+ // Check for AI assistance
+ aiAssisted := detectAIUsage(repoPath)
+
projectSummary := &ProjectSummary{
- Name: repoName,
- Summary: summary,
- CodebergURL: codebergURL,
- GitHubURL: githubURL,
- Metadata: metadata,
- Images: images,
+ Name: repoName,
+ Summary: summary,
+ CodebergURL: codebergURL,
+ GitHubURL: githubURL,
+ Metadata: metadata,
+ Images: images,
+ CodeSnippet: codeSnippet,
+ CodeLanguage: codeLanguage,
+ AIAssisted: aiAssisted,
}
// Save to cache
@@ -359,17 +380,17 @@ func (g *Generator) formatGemtext(summaries []ProjectSummary) string {
// Write total stats section
builder.WriteString("## Overall Statistics\n\n")
- builder.WriteString(fmt.Sprintf("* Total Projects: %d\n", totalProjects))
- builder.WriteString(fmt.Sprintf("* Total Commits: %s\n", formatNumber(totalCommits)))
- builder.WriteString(fmt.Sprintf("* Total Lines of Code: %s\n", formatNumber(totalLOC)))
+ builder.WriteString(fmt.Sprintf("* 📦 Total Projects: %d\n", totalProjects))
+ builder.WriteString(fmt.Sprintf("* 📊 Total Commits: %s\n", formatNumber(totalCommits)))
+ builder.WriteString(fmt.Sprintf("* 📈 Total Lines of Code: %s\n", formatNumber(totalLOC)))
if totalDocs > 0 {
- builder.WriteString(fmt.Sprintf("* Total Lines of Documentation: %s\n", formatNumber(totalDocs)))
+ builder.WriteString(fmt.Sprintf("* 📄 Total Lines of Documentation: %s\n", formatNumber(totalDocs)))
}
if len(languageStats) > 0 {
- builder.WriteString(fmt.Sprintf("* Languages: %s\n", FormatLanguagesWithPercentages(languageStats)))
+ builder.WriteString(fmt.Sprintf("* 💻 Languages: %s\n", FormatLanguagesWithPercentages(languageStats)))
}
if len(docStats) > 0 {
- builder.WriteString(fmt.Sprintf("* Documentation: %s\n", FormatLanguagesWithPercentages(docStats)))
+ builder.WriteString(fmt.Sprintf("* 📚 Documentation: %s\n", FormatLanguagesWithPercentages(docStats)))
}
builder.WriteString("\n")
@@ -389,19 +410,24 @@ 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", FormatLanguagesWithPercentages(summary.Metadata.Languages)))
+ builder.WriteString(fmt.Sprintf("* 💻 Languages: %s\n", FormatLanguagesWithPercentages(summary.Metadata.Languages)))
}
if len(summary.Metadata.Documentation) > 0 {
- builder.WriteString(fmt.Sprintf("* Documentation: %s\n", FormatLanguagesWithPercentages(summary.Metadata.Documentation)))
+ builder.WriteString(fmt.Sprintf("* 📚 Documentation: %s\n", FormatLanguagesWithPercentages(summary.Metadata.Documentation)))
}
- builder.WriteString(fmt.Sprintf("* Commits: %d\n", summary.Metadata.CommitCount))
- builder.WriteString(fmt.Sprintf("* Lines of Code: %d\n", summary.Metadata.LinesOfCode))
+ builder.WriteString(fmt.Sprintf("* 📊 Commits: %d\n", summary.Metadata.CommitCount))
+ builder.WriteString(fmt.Sprintf("* 📈 Lines of Code: %d\n", summary.Metadata.LinesOfCode))
if summary.Metadata.LinesOfDocs > 0 {
- builder.WriteString(fmt.Sprintf("* Lines of Documentation: %d\n", summary.Metadata.LinesOfDocs))
+ builder.WriteString(fmt.Sprintf("* 📄 Lines of Documentation: %d\n", summary.Metadata.LinesOfDocs))
+ }
+ builder.WriteString(fmt.Sprintf("* 📅 Development Period: %s to %s\n", summary.Metadata.FirstCommitDate, summary.Metadata.LastCommitDate))
+ builder.WriteString(fmt.Sprintf("* 🔥 Recent Activity: %.1f days (avg. age of last 42 commits)\n", summary.Metadata.AvgCommitAge))
+ builder.WriteString(fmt.Sprintf("* ⚖️ License: %s\n", summary.Metadata.License))
+
+ // Add AI-Assisted notice if detected
+ if summary.AIAssisted {
+ builder.WriteString("* 🤖 AI-Assisted: This project was partially generated with the help of generative AI\n")
}
- builder.WriteString(fmt.Sprintf("* Development Period: %s to %s\n", summary.Metadata.FirstCommitDate, summary.Metadata.LastCommitDate))
- builder.WriteString(fmt.Sprintf("* Recent Activity: %.1f days (avg. age of last 42 commits)\n", summary.Metadata.AvgCommitAge))
- builder.WriteString(fmt.Sprintf("* License: %s\n", summary.Metadata.License))
// Check if project might be obsolete (avg age > 2 years AND last commit > 1 year)
if summary.Metadata.AvgCommitAge > 730 && summary.Metadata.LastCommitDate != "" {
@@ -440,7 +466,7 @@ func (g *Generator) formatGemtext(summaries []ProjectSummary) string {
builder.WriteString(fmt.Sprintf("%s\n\n", strings.TrimSpace(paragraphs[i])))
}
} else {
- // No images, just add paragraphs normally
+ // No images - just add all paragraphs
for _, para := range paragraphs {
builder.WriteString(fmt.Sprintf("%s\n\n", strings.TrimSpace(para)))
}
@@ -453,6 +479,11 @@ func (g *Generator) formatGemtext(summaries []ProjectSummary) string {
if summary.GitHubURL != "" {
builder.WriteString(fmt.Sprintf("=> %s View on GitHub\n", summary.GitHubURL))
}
+
+ // Add code snippet at the end (no header, just the code)
+ if summary.CodeSnippet != "" && len(summary.Images) == 0 {
+ builder.WriteString(fmt.Sprintf("\n%s:\n\n```\n%s\n```\n", summary.CodeLanguage, summary.CodeSnippet))
+ }
}
return builder.String()
@@ -647,4 +678,30 @@ func formatNumber(n int) string {
}
return string(result)
+}
+
+// detectAIUsage checks if the repository was generated with AI assistance
+func detectAIUsage(repoPath string) bool {
+ // Check for AI-related files
+ aiFiles := []string{"CLAUDE.md", "GEMINI.md"}
+ for _, aiFile := range aiFiles {
+ filePath := filepath.Join(repoPath, aiFile)
+ if _, err := os.Stat(filePath); err == nil {
+ return true
+ }
+ }
+
+ // Search for "agentic coding" string in the repository
+ cmd := exec.Command("rg", "-i", "--max-count", "1", "agentic coding", repoPath)
+ if output, err := cmd.Output(); err == nil && len(output) > 0 {
+ return true
+ }
+
+ // Fallback to grep if rg is not available
+ cmd = exec.Command("grep", "-r", "-i", "-m", "1", "agentic coding", repoPath)
+ if output, err := cmd.Output(); err == nil && len(output) > 0 {
+ return true
+ }
+
+ return false
} \ No newline at end of file