Init commit
Some checks failed
CI/CD Pipeline / Build Docker Image (push) Has been cancelled
CI/CD Pipeline / Test (push) Has been cancelled
CI/CD Pipeline / Build (arm64, windows, linkbeam-windows-arm64.exe) (push) Has been cancelled
CI/CD Pipeline / Build (386, linux, linkbeam-linux-386) (push) Has been cancelled
CI/CD Pipeline / Lint (push) Has been cancelled
CI/CD Pipeline / Build (amd64, linux, linkbeam-linux-amd64) (push) Has been cancelled
CI/CD Pipeline / Build (arm, 7, linux, linkbeam-linux-armv7) (push) Has been cancelled
CI/CD Pipeline / Build (386, windows, linkbeam-windows-386.exe) (push) Has been cancelled
CI/CD Pipeline / Build (amd64, windows, linkbeam-windows-amd64.exe) (push) Has been cancelled
CI/CD Pipeline / Build (arm64, darwin, linkbeam-darwin-arm64) (push) Has been cancelled
CI/CD Pipeline / Build (arm64, linux, linkbeam-linux-arm64) (push) Has been cancelled
CI/CD Pipeline / Build (amd64, darwin, linkbeam-darwin-amd64) (push) Has been cancelled
CI/CD Pipeline / Create Release (push) Has been cancelled

This commit is contained in:
2025-10-12 21:56:53 +04:00
commit 20f949c250
42 changed files with 4478 additions and 0 deletions

111
internal/config/config.go Normal file
View File

@@ -0,0 +1,111 @@
// config.go
/*
* Copyright (c) - All Rights Reserved.
*
* See the LICENSE file for more information.
*/
package config
import (
"errors"
"os"
"strings"
)
type Config struct {
Name string `yaml:"name"`
Bio string `yaml:"bio"`
Avatar string `yaml:"avatar"`
Theme string `yaml:"theme"`
FontAwesomeCDN string `yaml:"font_awesome_cdn"`
Footer []FooterBlock `yaml:"footer"`
Links []Link `yaml:"links"`
Socials []Social `yaml:"socials"`
}
type Link struct {
Title string `yaml:"title"`
URL string `yaml:"url"`
Icon string `yaml:"icon"`
}
type Social struct {
Platform string `yaml:"platform"`
URL string `yaml:"url"`
Icon string `yaml:"icon"`
}
type FooterBlock struct {
Text string `yaml:"text"`
}
func (c *Config) LinksCount() int {
return len(c.Links)
}
// GetAvailableThemes discovers available themes from the themes directory.
// It returns a list of theme names (without the .css extension).
func GetAvailableThemes(themesDir string) ([]string, error) {
entries, err := os.ReadDir(themesDir)
if err != nil {
// If themes directory doesn't exist, return empty list
if os.IsNotExist(err) {
return []string{}, nil
}
return nil, err
}
var themes []string
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if strings.HasSuffix(name, ".css") {
themeName := strings.TrimSuffix(name, ".css")
themes = append(themes, themeName)
}
}
return themes, nil
}
func (c *Config) Validate() error {
return c.ValidateWithThemes("")
}
// ValidateWithThemes validates the config with theme discovery.
// If themesDir is empty, theme validation is skipped.
func (c *Config) ValidateWithThemes(themesDir string) error {
if c.Name == "" {
return errors.New("name cannot be empty")
}
// Skip theme validation if no themes directory provided
if themesDir == "" {
return nil
}
availableThemes, err := GetAvailableThemes(themesDir)
if err != nil {
return err
}
// If no themes found, skip validation
if len(availableThemes) == 0 {
return nil
}
// Check if the configured theme is available
validThemes := make(map[string]bool)
for _, theme := range availableThemes {
validThemes[theme] = true
}
if !validThemes[c.Theme] {
return errors.New("theme '" + c.Theme + "' not found. Available themes: " + strings.Join(availableThemes, ", "))
}
return nil
}

View File

@@ -0,0 +1,98 @@
// config_test.go
/*
* Copyright (c) - All Rights Reserved.
*
* See the LICENSE file for more information.
*/
package config
import (
"testing"
)
func loadTestConfig(t *testing.T) *Config {
t.Helper()
cfg, err := Load("testdata/config.yaml")
if err != nil {
t.Fatalf("load config: %v", err)
}
return cfg
}
func TestLoad(t *testing.T) {
cfg := loadTestConfig(t)
tests := []struct {
name string
got interface{}
want interface{}
}{
{"name", cfg.Name, "Ada Lovelace"},
{"links count", len(cfg.Links), 2},
{"first link URL", cfg.Links[0].URL, "https://ada.blog"},
}
for _, tt := range tests {
if tt.got != tt.want {
t.Errorf("%s: got %v, want %v", tt.name, tt.got, tt.want)
}
}
}
func TestThemeValidation(t *testing.T) {
cfg := loadTestConfig(t)
if cfg.Theme != "light" && cfg.Theme != "dark" && cfg.Theme != "auto" {
t.Errorf("invalid theme: %s", cfg.Theme)
}
}
func TestLinksCountMethod(t *testing.T) {
cfg := loadTestConfig(t)
if got, want := cfg.LinksCount(), 2; got != want {
t.Errorf("LinksCount() = %d, want %d", got, want)
}
}
func TestFooterBlocks(t *testing.T) {
tests := []struct {
name string
cfg Config
want int
}{
{
name: "with footer blocks",
cfg: Config{
Name: "Test",
Footer: []FooterBlock{
{Text: "© 2025 Test"},
{Text: "Made with LinkBeam"},
},
},
want: 2,
},
{
name: "empty footer",
cfg: Config{Name: "Test", Footer: []FooterBlock{}},
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := len(tt.cfg.Footer); got != tt.want {
t.Errorf("Footer length = %d, want %d", got, tt.want)
}
})
}
}
func TestFooterBlockText(t *testing.T) {
block := FooterBlock{Text: "Test Footer"}
want := "Test Footer"
if block.Text != want {
t.Errorf("FooterBlock.Text = %q, want %q", block.Text, want)
}
}

29
internal/config/load.go Normal file
View File

@@ -0,0 +1,29 @@
// load.go
/*
* Copyright (c) - All Rights Reserved.
*
* See the LICENCE file for more information.
*/
package config
import (
"os"
"gopkg.in/yaml.v3"
)
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return &cfg, cfg.Validate()
}

View File

@@ -0,0 +1,34 @@
// load_test.go
/*
* Copyright (c) - All Rights Reserved.
*
* See the LICENCE file for more information.
*/
package config
import (
"os"
"path/filepath"
"testing"
)
func TestLoad_Validate(t *testing.T) {
tmpDir := t.TempDir()
badConfigPath := filepath.Join(tmpDir, "bad.yaml")
// Write invalid config (empty name, invalid theme)
content := []byte(`
name: ""
theme: "unknown"
`)
if err := os.WriteFile(badConfigPath, content, 0644); err != nil {
t.Fatalf("failed to write bad config: %v", err)
}
_, err := Load(badConfigPath)
if err == nil {
t.Error("expected error for invalid config, got nil")
}
}

View File

@@ -0,0 +1,23 @@
# config-catppuccin-frappe.yaml - Test config with Catppuccin Frappé theme
---
name: "Test User Catppuccin"
bio: "Testing Catppuccin Frappé theme"
avatar: "static/logo.png"
theme: "catppuccin-frappe"
footer:
- text: "© 2025 Test User"
- text: "Catppuccin Frappé Theme"
links:
- title: "My Website"
url: "https://example.com"
icon: "fas fa-globe"
- title: "GitHub"
url: "https://github.com/user"
icon: "fab fa-github"
socials:
- platform: "Twitter"
url: "https://twitter.com/user"
icon: "fab fa-twitter"

View File

@@ -0,0 +1,23 @@
# config-catppuccin-latte.yaml - Test config with Catppuccin Latte theme
---
name: "Test User Catppuccin"
bio: "Testing Catppuccin Latte theme"
avatar: "static/logo.png"
theme: "catppuccin-latte"
footer:
- text: "© 2025 Test User"
- text: "Catppuccin Latte Theme"
links:
- title: "My Website"
url: "https://example.com"
icon: "fas fa-globe"
- title: "GitHub"
url: "https://github.com/user"
icon: "fab fa-github"
socials:
- platform: "Twitter"
url: "https://twitter.com/user"
icon: "fab fa-twitter"

View File

@@ -0,0 +1,23 @@
# config-catppuccin-macchiato.yaml - Test config with Catppuccin Macchiato theme
---
name: "Test User Catppuccin"
bio: "Testing Catppuccin Macchiato theme"
avatar: "static/logo.png"
theme: "catppuccin-macchiato"
footer:
- text: "© 2025 Test User"
- text: "Catppuccin Macchiato Theme"
links:
- title: "My Website"
url: "https://example.com"
icon: "fas fa-globe"
- title: "GitHub"
url: "https://github.com/user"
icon: "fab fa-github"
socials:
- platform: "Twitter"
url: "https://twitter.com/user"
icon: "fab fa-twitter"

View File

@@ -0,0 +1,23 @@
# config-catppuccin.yaml - Test config with Catppuccin theme
---
name: "Test User Catppuccin"
bio: "Testing Catppuccin Mocha theme"
avatar: "static/logo.png"
theme: "catppuccin-mocha"
footer:
- text: "© 2025 Test User"
- text: "Catppuccin Mocha Theme"
links:
- title: "My Website"
url: "https://example.com"
icon: "fas fa-globe"
- title: "GitHub"
url: "https://github.com/user"
icon: "fab fa-github"
socials:
- platform: "Twitter"
url: "https://twitter.com/user"
icon: "fab fa-twitter"

View File

@@ -0,0 +1,23 @@
# config-gruvbox.yaml - Test config with Gruvbox theme
---
name: "Test User Gruvbox"
bio: "Testing Gruvbox theme with light/dark variants"
avatar: "static/logo.png"
theme: "gruvbox"
footer:
- text: "© 2025 Test User"
- text: "Gruvbox Theme"
links:
- title: "My Website"
url: "https://example.com"
icon: "fas fa-globe"
- title: "GitHub"
url: "https://github.com/user"
icon: "fab fa-github"
socials:
- platform: "Twitter"
url: "https://twitter.com/user"
icon: "fab fa-twitter"

View File

@@ -0,0 +1,12 @@
# config-invalid-theme.yaml - Test config with invalid theme (for error testing)
---
name: "Test User Invalid"
bio: "Testing invalid theme error handling"
avatar: "static/logo.png"
theme: "nonexistent"
links:
- title: "My Website"
url: "https://example.com"
icon: "fas fa-globe"

View File

@@ -0,0 +1,23 @@
# config-nord.yaml - Test config with Nord theme
---
name: "Test User Nord"
bio: "Testing Nord theme with light/dark variants"
avatar: "static/logo.png"
theme: "nord"
footer:
- text: "© 2025 Test User"
- text: "Nord Theme"
links:
- title: "My Website"
url: "https://example.com"
icon: "fas fa-globe"
- title: "GitHub"
url: "https://github.com/user"
icon: "fab fa-github"
socials:
- platform: "Twitter"
url: "https://twitter.com/user"
icon: "fab fa-twitter"

View File

@@ -0,0 +1,128 @@
// validate_test.go
/*
* Copyright (c) - All Rights Reserved.
*
* See the LICENCE file for more information.
*/
package config
import (
"os"
"path/filepath"
"testing"
)
func TestValidateConfig(t *testing.T) {
tests := []struct {
name string
cfg Config
wantError bool
}{
{"missing name", Config{Name: "", Theme: "auto"}, true},
{"valid config", Config{Name: "Ada", Theme: "auto"}, false},
{"valid config with any theme", Config{Name: "Ada", Theme: "invalid"}, false}, // No theme validation without themesDir
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.cfg.Validate()
if (err != nil) != tt.wantError {
t.Errorf("Validate() error = %v, wantError %v", err, tt.wantError)
}
})
}
}
func TestValidateWithThemes(t *testing.T) {
// Create a temporary themes directory for testing
tmpDir := t.TempDir()
themesDir := filepath.Join(tmpDir, "themes")
if err := os.MkdirAll(themesDir, 0755); err != nil {
t.Fatal(err)
}
// Create some test theme files
for _, theme := range []string{"auto", "nord", "gruvbox", "catppuccin"} {
if err := os.WriteFile(filepath.Join(themesDir, theme+".css"), []byte("/* test */"), 0644); err != nil {
t.Fatal(err)
}
}
tests := []struct {
name string
cfg Config
themesDir string
wantError bool
}{
{"missing name", Config{Name: "", Theme: "auto"}, themesDir, true},
{"invalid theme", Config{Name: "Ada", Theme: "invalid"}, themesDir, true},
{"valid auto theme", Config{Name: "Ada", Theme: "auto"}, themesDir, false},
{"valid nord theme", Config{Name: "Ada", Theme: "nord"}, themesDir, false},
{"valid gruvbox theme", Config{Name: "Ada", Theme: "gruvbox"}, themesDir, false},
{"valid catppuccin theme", Config{Name: "Ada", Theme: "catppuccin"}, themesDir, false},
{"no themes dir", Config{Name: "Ada", Theme: "anything"}, "", false}, // Skip validation if no dir
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.cfg.ValidateWithThemes(tt.themesDir)
if (err != nil) != tt.wantError {
t.Errorf("ValidateWithThemes() error = %v, wantError %v", err, tt.wantError)
}
})
}
}
func TestGetAvailableThemes(t *testing.T) {
// Create a temporary themes directory for testing
tmpDir := t.TempDir()
themesDir := filepath.Join(tmpDir, "themes")
if err := os.MkdirAll(themesDir, 0755); err != nil {
t.Fatal(err)
}
// Create some test theme files
testThemes := []string{"auto", "nord", "gruvbox"}
for _, theme := range testThemes {
if err := os.WriteFile(filepath.Join(themesDir, theme+".css"), []byte("/* test */"), 0644); err != nil {
t.Fatal(err)
}
}
// Create a non-CSS file (should be ignored)
if err := os.WriteFile(filepath.Join(themesDir, "readme.txt"), []byte("readme"), 0644); err != nil {
t.Fatal(err)
}
themes, err := GetAvailableThemes(themesDir)
if err != nil {
t.Fatalf("GetAvailableThemes() error = %v", err)
}
if len(themes) != len(testThemes) {
t.Errorf("GetAvailableThemes() got %d themes, want %d", len(themes), len(testThemes))
}
// Check that all expected themes are present
themeMap := make(map[string]bool)
for _, theme := range themes {
themeMap[theme] = true
}
for _, expectedTheme := range testThemes {
if !themeMap[expectedTheme] {
t.Errorf("Expected theme %q not found in available themes", expectedTheme)
}
}
// Test non-existent directory
themes, err = GetAvailableThemes(filepath.Join(tmpDir, "nonexistent"))
if err != nil {
t.Errorf("GetAvailableThemes() should not error on non-existent dir, got %v", err)
}
if len(themes) != 0 {
t.Errorf("GetAvailableThemes() should return empty list for non-existent dir, got %d themes", len(themes))
}
}