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