@@ -6,21 +6,144 @@ package ansifonts
66import (
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
1418var 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
17132func 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
39162func 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}
0 commit comments