Skip to content

Commit 0220ca1

Browse files
feature: add smart .bit font or folder loading via -load cli flag
1 parent 705b4d6 commit 0220ca1

File tree

3 files changed

+180
-62
lines changed

3 files changed

+180
-62
lines changed

ansifonts/loader.go

Lines changed: 145 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,144 @@ package ansifonts
66
import (
77
"embed"
88
"encoding/json"
9+
"fmt"
10+
"os"
911
"path"
12+
"path/filepath"
13+
"sort"
1014
"strings"
1115
)
1216

1317
//go:embed fonts/*.bit
1418
var EmbeddedFonts embed.FS
1519

16-
// LoadFont loads a font by name from the embedded fonts directory
20+
// customFontsRegistry holds custom fonts loaded from the filesystem
21+
var customFontsRegistry = make(map[string]FontData)
22+
23+
// validateFontData ensures the JSON has required fields
24+
func validateFontData(fd *FontData) error {
25+
if fd.Name == "" {
26+
return fmt.Errorf("font data missing required 'name' field")
27+
}
28+
if fd.Characters == nil || len(fd.Characters) == 0 {
29+
return fmt.Errorf("font data missing required 'characters' field")
30+
}
31+
return nil
32+
}
33+
34+
// RegisterFontFile loads a single .bit font file and registers it
35+
func RegisterFontFile(path string) (string, error) {
36+
// Check file extension (case-insensitive)
37+
ext := strings.ToLower(filepath.Ext(path))
38+
if ext != ".bit" {
39+
return "", fmt.Errorf("file %s does not have .bit extension", path)
40+
}
41+
42+
// Read file
43+
fontBytes, err := os.ReadFile(path)
44+
if err != nil {
45+
return "", fmt.Errorf("failed to read file %s: %w", path, err)
46+
}
47+
48+
// Unmarshal and validate
49+
var fontData FontData
50+
if err := json.Unmarshal(fontBytes, &fontData); err != nil {
51+
return "", fmt.Errorf("failed to parse JSON in %s: %w", path, err)
52+
}
53+
54+
if err := validateFontData(&fontData); err != nil {
55+
return "", fmt.Errorf("invalid font data in %s: %w", path, err)
56+
}
57+
58+
// Store in registry using lowercase name as key
59+
key := strings.ToLower(fontData.Name)
60+
customFontsRegistry[key] = fontData
61+
62+
return fontData.Name, nil
63+
}
64+
65+
// RegisterFontDirectory loads all .bit font files from a directory
66+
func RegisterFontDirectory(dirPath string) ([]string, error) {
67+
entries, err := os.ReadDir(dirPath)
68+
if err != nil {
69+
return nil, fmt.Errorf("failed to read directory %s: %w", dirPath, err)
70+
}
71+
72+
var loadedNames []string
73+
var errors []string
74+
75+
for _, entry := range entries {
76+
if entry.IsDir() {
77+
continue
78+
}
79+
80+
fileName := entry.Name()
81+
if !strings.HasSuffix(strings.ToLower(fileName), ".bit") {
82+
continue
83+
}
84+
85+
fullPath := filepath.Join(dirPath, fileName)
86+
fontName, err := RegisterFontFile(fullPath)
87+
if err != nil {
88+
errors = append(errors, fmt.Sprintf("failed to load %s: %v", fileName, err))
89+
continue
90+
}
91+
92+
loadedNames = append(loadedNames, fontName)
93+
}
94+
95+
// If no fonts could be loaded, return an error
96+
if len(loadedNames) == 0 {
97+
if len(errors) > 0 {
98+
return nil, fmt.Errorf("no fonts could be loaded from directory %s. Errors: %s", dirPath, strings.Join(errors, "; "))
99+
}
100+
return nil, fmt.Errorf("no .bit font files found in directory %s", dirPath)
101+
}
102+
103+
// Log errors for partially failed loads (but still return success)
104+
if len(errors) > 0 {
105+
fmt.Fprintf(os.Stderr, "Warning: Some fonts failed to load: %s\n", strings.Join(errors, "; "))
106+
}
107+
108+
return loadedNames, nil
109+
}
110+
111+
// RegisterCustomPath is the smart entry point that handles both files and directories
112+
func RegisterCustomPath(path string) ([]string, error) {
113+
info, err := os.Stat(path)
114+
if err != nil {
115+
return nil, fmt.Errorf("path %s does not exist: %w", path, err)
116+
}
117+
118+
if info.IsDir() {
119+
return RegisterFontDirectory(path)
120+
}
121+
122+
// It's a file
123+
fontName, err := RegisterFontFile(path)
124+
if err != nil {
125+
return nil, err
126+
}
127+
128+
return []string{fontName}, nil
129+
}
130+
131+
// LoadFont loads a font by name, checking custom fonts first, then embedded fonts
17132
func LoadFont(name string) (*Font, error) {
18-
// Construct the path using forward slashes for embedded filesystem
19-
fontPath := path.Join("fonts", name+".bit")
133+
// Check custom fonts registry first (allows overriding embedded fonts)
134+
key := strings.ToLower(name)
135+
if fontData, exists := customFontsRegistry[key]; exists {
136+
return &Font{
137+
Name: fontData.Name,
138+
FontData: fontData,
139+
}, nil
140+
}
20141

142+
// Fall back to embedded fonts
143+
fontPath := path.Join("fonts", name+".bit")
21144
fontBytes, err := EmbeddedFonts.ReadFile(fontPath)
22145
if err != nil {
23-
return nil, err
146+
return nil, fmt.Errorf("font '%s' not found in custom or embedded fonts", name)
24147
}
25148

26149
var fontData FontData
@@ -35,20 +158,35 @@ func LoadFont(name string) (*Font, error) {
35158
}, nil
36159
}
37160

38-
// ListFonts returns a list of available font names from the embedded fonts directory
161+
// ListFonts returns a list of available font names from both custom and embedded fonts
39162
func ListFonts() ([]string, error) {
163+
// Get embedded fonts
40164
entries, err := EmbeddedFonts.ReadDir("fonts")
41165
if err != nil {
42166
return nil, err
43167
}
44168

45-
var fonts []string
169+
fontSet := make(map[string]bool)
170+
171+
// Add embedded fonts
46172
for _, entry := range entries {
47173
if !entry.IsDir() && path.Ext(entry.Name()) == ".bit" {
48174
fontName := strings.TrimSuffix(entry.Name(), ".bit")
49-
fonts = append(fonts, fontName)
175+
fontSet[fontName] = true
50176
}
51177
}
52178

179+
// Add custom fonts (these may override embedded fonts)
180+
for _, fontData := range customFontsRegistry {
181+
fontSet[fontData.Name] = true
182+
}
183+
184+
// Convert to sorted slice
185+
fonts := make([]string, 0, len(fontSet))
186+
for fontName := range fontSet {
187+
fonts = append(fonts, fontName)
188+
}
189+
sort.Strings(fonts)
190+
53191
return fonts, nil
54192
}

cmd/bit/main.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func main() {
2828
var alignment string
2929
var list bool
3030
var version bool
31+
var loadFontPath string
3132

3233
flag.StringVar(&fontName, "font", "", "Font name to use (default: first available font)")
3334
flag.StringVar(&textColor, "color", "", "Text color: ANSI code (31) or hex (#FF0000)")
@@ -44,6 +45,7 @@ func main() {
4445
flag.StringVar(&alignment, "align", "center", "Text alignment: left, center, right")
4546
flag.BoolVar(&list, "list", false, "List all available fonts")
4647
flag.BoolVar(&version, "version", false, "Show version information")
48+
flag.StringVar(&loadFontPath, "load", "", "Path to a custom font file (.bit) OR a directory of fonts")
4749

4850
flag.Usage = func() {
4951
fmt.Fprintf(os.Stderr, "Bit - Terminal ANSI Logo Designer & Font Library\n\n")
@@ -65,10 +67,22 @@ func main() {
6567
fmt.Fprintf(os.Stderr, " bit -font ithaca -color \"#FF0000\" \"Red Hex\" # Hex color\n")
6668
fmt.Fprintf(os.Stderr, " bit -font dogica -color 31 -gradient 34 \"Gradient\" # Gradient\n")
6769
fmt.Fprintf(os.Stderr, " bit -font pressstart -color 32 -shadow \"Shadow\" # With shadow\n")
70+
fmt.Fprintf(os.Stderr, " bit -load ./myfont.bit \"Custom\" # Load custom font file\n")
71+
fmt.Fprintf(os.Stderr, " bit -load ./fonts/ -list # Load custom font directory\n")
6872
}
6973

7074
flag.Parse()
7175

76+
// Process custom font loading BEFORE other operations
77+
if loadFontPath != "" {
78+
loadedFonts, err := ansifonts.RegisterCustomPath(loadFontPath)
79+
if err != nil {
80+
fmt.Fprintf(os.Stderr, "Error loading custom fonts from '%s': %v\n", loadFontPath, err)
81+
os.Exit(1)
82+
}
83+
fmt.Fprintf(os.Stderr, "Loaded %d custom fonts: %v\n", len(loadedFonts), loadedFonts)
84+
}
85+
7286
// Show version
7387
if version {
7488
fmt.Println("Bit - Terminal ANSI Logo Designer & Font Library")

internal/ui/font_loader.go

Lines changed: 21 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,33 @@
11
package ui
22

33
import (
4-
"encoding/json"
54
"fmt"
6-
"path"
7-
"sort"
8-
"strings"
95

106
"github.com/superstarryeyes/bit/ansifonts"
117
)
128

139
// loadFontList loads only the font metadata without loading the actual font data
1410
// This provides a list of available fonts without consuming memory for all font data
1511
func loadFontList() ([]FontInfo, error) {
16-
var fonts []FontInfo
17-
18-
// Read from embedded filesystem in ansifonts package
19-
entries, err := ansifonts.EmbeddedFonts.ReadDir("fonts")
12+
// Use the unified ListFonts function which includes both custom and embedded fonts
13+
fontNames, err := ansifonts.ListFonts()
2014
if err != nil {
21-
return nil, fmt.Errorf("failed to read embedded fonts directory: %w", err)
15+
return nil, fmt.Errorf("failed to list fonts: %w", err)
2216
}
2317

24-
for _, entry := range entries {
25-
if path.Ext(entry.Name()) == ".bit" {
26-
fontPath := path.Join("fonts", entry.Name())
27-
28-
// For lazy loading, we only load the font name from the file
29-
// This is much more efficient than loading the entire font data
30-
fontDataBytes, err := ansifonts.EmbeddedFonts.ReadFile(fontPath)
31-
if err != nil {
32-
// Log or track skipped files for debugging
33-
fmt.Printf("Warning: skipping font file %s: %v\n", entry.Name(), err)
34-
continue
35-
}
36-
37-
var fontMetadata struct {
38-
Name string `json:"name"`
39-
}
40-
err = json.Unmarshal(fontDataBytes, &fontMetadata)
41-
if err != nil {
42-
// Log or track invalid JSON files for debugging
43-
fmt.Printf("Warning: skipping invalid font JSON %s: %v\n", entry.Name(), err)
44-
continue
45-
}
46-
47-
fonts = append(fonts, FontInfo{
48-
Name: fontMetadata.Name,
49-
Path: fontPath,
50-
Loaded: false, // Font data not loaded yet
51-
})
52-
}
18+
if len(fontNames) == 0 {
19+
return nil, fmt.Errorf("no fonts available - please ensure font files are properly embedded or loaded")
5320
}
5421

55-
if len(fonts) == 0 {
56-
return nil, fmt.Errorf("no valid fonts found in embedded directory - please ensure font files are properly embedded")
22+
var fonts []FontInfo
23+
for _, fontName := range fontNames {
24+
fonts = append(fonts, FontInfo{
25+
Name: fontName,
26+
Path: "", // Path not relevant when using unified loader
27+
Loaded: false, // Font data not loaded yet
28+
})
5729
}
5830

59-
// Sort fonts case-insensitively by name
60-
sort.Slice(fonts, func(i, j int) bool {
61-
return strings.ToLower(fonts[i].Name) < strings.ToLower(fonts[j].Name)
62-
})
63-
6431
return fonts, nil
6532
}
6633

@@ -72,20 +39,19 @@ func loadFontData(font *FontInfo) error {
7239
return nil
7340
}
7441

75-
// Load the full font data
76-
fontDataBytes, err := ansifonts.EmbeddedFonts.ReadFile(font.Path)
42+
// Use the unified LoadFont function which handles both custom and embedded fonts
43+
loadedFont, err := ansifonts.LoadFont(font.Name)
7744
if err != nil {
78-
return fmt.Errorf("failed to read font file %s: %w", font.Path, err)
45+
return fmt.Errorf("failed to load font %s: %w", font.Name, err)
7946
}
8047

81-
var fontData FontData
82-
err = json.Unmarshal(fontDataBytes, &fontData)
83-
if err != nil {
84-
return fmt.Errorf("failed to parse font JSON %s: %w", font.Path, err)
48+
// Update the font with loaded data - convert ansifonts.FontData to ui.FontData
49+
font.FontData = &FontData{
50+
Name: loadedFont.FontData.Name,
51+
Author: loadedFont.FontData.Author,
52+
License: loadedFont.FontData.License,
53+
Characters: loadedFont.FontData.Characters,
8554
}
86-
87-
// Update the font with loaded data
88-
font.FontData = &fontData
8955
font.Loaded = true
9056

9157
return nil

0 commit comments

Comments
 (0)