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