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)
 16
 17type ProviderClient interface {
 18	GetProviders() ([]catwalk.Provider, error)
 19}
 20
 21var (
 22	providerOnce sync.Once
 23	providerList []catwalk.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 []catwalk.Provider) error {
 48	slog.Info("Saving cached provider data", "path", path)
 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) ([]catwalk.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 []catwalk.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() ([]catwalk.Provider, error) {
 78	catwalkURL := cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL)
 79	client := catwalk.NewWithURL(catwalkURL)
 80	path := providerCacheFileData()
 81	return loadProvidersOnce(client, path)
 82}
 83
 84func loadProvidersOnce(client ProviderClient, path string) ([]catwalk.Provider, error) {
 85	var err error
 86	providerOnce.Do(func() {
 87		providerList, err = loadProviders(client, path)
 88	})
 89	if err != nil {
 90		return nil, err
 91	}
 92	return providerList, nil
 93}
 94
 95func loadProviders(client ProviderClient, path string) (providerList []catwalk.Provider, err error) {
 96	// if cache is not stale, load from it
 97	stale, exists := isCacheStale(path)
 98	if !stale {
 99		slog.Info("Using cached provider data", "path", path)
100		providerList, err = loadProvidersFromCache(path)
101		if len(providerList) > 0 && err == nil {
102			go func() {
103				slog.Info("Updating provider cache in background", "path", path)
104				updated, uerr := client.GetProviders()
105				if len(updated) > 0 && uerr == nil {
106					_ = saveProvidersInCache(path, updated)
107				}
108			}()
109			return
110		}
111	}
112
113	slog.Info("Getting live provider data", "path", path)
114	providerList, err = client.GetProviders()
115	if len(providerList) > 0 && err == nil {
116		err = saveProvidersInCache(path, providerList)
117		return
118	}
119	if !exists {
120		err = fmt.Errorf("failed to load providers")
121		return
122	}
123	slog.Info("Loading provider data from cache", "path", path)
124	providerList, err = loadProvidersFromCache(path)
125	return
126}
127
128func isCacheStale(path string) (stale, exists bool) {
129	info, err := os.Stat(path)
130	if err != nil {
131		return true, false
132	}
133	return time.Since(info.ModTime()) > 24*time.Hour, true
134}