1package config
2
3import (
4 "encoding/json"
5 "fmt"
6 "log/slog"
7 "os"
8 "path/filepath"
9 "runtime"
10 "sync"
11 "time"
12
13 "github.com/charmbracelet/crush/internal/fur/client"
14 "github.com/charmbracelet/crush/internal/fur/provider"
15)
16
17type ProviderClient interface {
18 GetProviders() ([]provider.Provider, error)
19}
20
21var (
22 providerOnce sync.Once
23 providerList []provider.Provider
24)
25
26// file to cache provider data
27func providerCacheFileData() string {
28 xdgDataHome := os.Getenv("XDG_DATA_HOME")
29 if xdgDataHome != "" {
30 return filepath.Join(xdgDataHome, appName, "providers.json")
31 }
32
33 // return the path to the main data directory
34 // for windows, it should be in `%LOCALAPPDATA%/crush/`
35 // for linux and macOS, it should be in `$HOME/.local/share/crush/`
36 if runtime.GOOS == "windows" {
37 localAppData := os.Getenv("LOCALAPPDATA")
38 if localAppData == "" {
39 localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
40 }
41 return filepath.Join(localAppData, appName, "providers.json")
42 }
43
44 return filepath.Join(os.Getenv("HOME"), ".local", "share", appName, "providers.json")
45}
46
47func saveProvidersInCache(path string, providers []provider.Provider) error {
48 slog.Info("Caching provider data")
49 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
50 return fmt.Errorf("failed to create directory for provider cache: %w", err)
51 }
52
53 data, err := json.MarshalIndent(providers, "", " ")
54 if err != nil {
55 return fmt.Errorf("failed to marshal provider data: %w", err)
56 }
57
58 if err := os.WriteFile(path, data, 0o644); err != nil {
59 return fmt.Errorf("failed to write provider data to cache: %w", err)
60 }
61 return nil
62}
63
64func loadProvidersFromCache(path string) ([]provider.Provider, error) {
65 data, err := os.ReadFile(path)
66 if err != nil {
67 return nil, fmt.Errorf("failed to read provider cache file: %w", err)
68 }
69
70 var providers []provider.Provider
71 if err := json.Unmarshal(data, &providers); err != nil {
72 return nil, fmt.Errorf("failed to unmarshal provider data from cache: %w", err)
73 }
74 return providers, nil
75}
76
77func Providers() ([]provider.Provider, error) {
78 client := client.New()
79 path := providerCacheFileData()
80 return loadProvidersOnce(client, path)
81}
82
83func loadProvidersOnce(client ProviderClient, path string) ([]provider.Provider, error) {
84 var err error
85 providerOnce.Do(func() {
86 providerList, err = loadProviders(client, path)
87 })
88 if err != nil {
89 return nil, err
90 }
91 return providerList, nil
92}
93
94func loadProviders(client ProviderClient, path string) (providerList []provider.Provider, err error) {
95 // if cache is not stale, load from it
96 stale, exists := isCacheStale(path)
97 if !stale {
98 slog.Info("Using cached provider data")
99 providerList, err = loadProvidersFromCache(path)
100 if len(providerList) > 0 && err == nil {
101 go func() {
102 slog.Info("Updating provider cache in background")
103 updated, uerr := client.GetProviders()
104 if len(updated) == 0 && uerr == nil {
105 _ = saveProvidersInCache(path, updated)
106 }
107 }()
108 return
109 }
110 }
111
112 slog.Info("Getting live provider data")
113 providerList, err = client.GetProviders()
114 if len(providerList) > 0 && err == nil {
115 err = saveProvidersInCache(path, providerList)
116 return
117 }
118 if !exists {
119 err = fmt.Errorf("failed to load providers")
120 return
121 }
122 providerList, err = loadProvidersFromCache(path)
123 return
124}
125
126func isCacheStale(path string) (stale, exists bool) {
127 info, err := os.Stat(path)
128 if err != nil {
129 return true, false
130 }
131 return time.Since(info.ModTime()) > 24*time.Hour, true
132}